@jwdobeutechsolutions/dobeutech-claude-code-custom 1.0.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/CLAUDE.md +174 -0
- package/CONTRIBUTING.md +191 -0
- package/README.md +345 -0
- package/agents/accessibility-auditor.md +315 -0
- package/agents/api-designer.md +265 -0
- package/agents/architect.md +211 -0
- package/agents/build-error-resolver.md +532 -0
- package/agents/ci-cd-generator.md +318 -0
- package/agents/code-reviewer.md +104 -0
- package/agents/database-migrator.md +258 -0
- package/agents/deployment-manager.md +296 -0
- package/agents/doc-updater.md +452 -0
- package/agents/docker-specialist.md +293 -0
- package/agents/e2e-runner.md +708 -0
- package/agents/fullstack-architect.md +293 -0
- package/agents/infrastructure-engineer.md +297 -0
- package/agents/integration-tester.md +320 -0
- package/agents/performance-tester.md +243 -0
- package/agents/planner.md +119 -0
- package/agents/refactor-cleaner.md +306 -0
- package/agents/security-reviewer.md +545 -0
- package/agents/tdd-guide.md +280 -0
- package/agents/unit-test-generator.md +290 -0
- package/bin/claude-config.js +290 -0
- package/commands/api-design.md +55 -0
- package/commands/audit-accessibility.md +37 -0
- package/commands/audit-performance.md +38 -0
- package/commands/audit-security.md +43 -0
- package/commands/build-fix.md +29 -0
- package/commands/changelog.md +31 -0
- package/commands/code-review.md +40 -0
- package/commands/deploy.md +51 -0
- package/commands/docs-api.md +41 -0
- package/commands/e2e.md +363 -0
- package/commands/plan.md +113 -0
- package/commands/refactor-clean.md +28 -0
- package/commands/tdd.md +326 -0
- package/commands/test-coverage.md +27 -0
- package/commands/update-codemaps.md +17 -0
- package/commands/update-docs.md +31 -0
- package/hooks/hooks.json +121 -0
- package/mcp-configs/mcp-servers.json +163 -0
- package/package.json +53 -0
- package/rules/agents.md +49 -0
- package/rules/coding-style.md +70 -0
- package/rules/git-workflow.md +45 -0
- package/rules/hooks.md +46 -0
- package/rules/patterns.md +55 -0
- package/rules/performance.md +47 -0
- package/rules/security.md +36 -0
- package/rules/testing.md +30 -0
- package/scripts/install.js +254 -0
- package/skills/backend-patterns.md +582 -0
- package/skills/clickhouse-io.md +429 -0
- package/skills/coding-standards.md +520 -0
- package/skills/frontend-patterns.md +631 -0
- package/skills/project-guidelines-example.md +345 -0
- package/skills/security-review/SKILL.md +494 -0
- package/skills/tdd-workflow/SKILL.md +409 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-patterns
|
|
3
|
+
description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Development Patterns
|
|
7
|
+
|
|
8
|
+
Modern frontend patterns for React, Next.js, and performant user interfaces.
|
|
9
|
+
|
|
10
|
+
## Component Patterns
|
|
11
|
+
|
|
12
|
+
### Composition Over Inheritance
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// ✅ GOOD: Component composition
|
|
16
|
+
interface CardProps {
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
variant?: 'default' | 'outlined'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Card({ children, variant = 'default' }: CardProps) {
|
|
22
|
+
return <div className={`card card-${variant}`}>{children}</div>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function CardHeader({ children }: { children: React.ReactNode }) {
|
|
26
|
+
return <div className="card-header">{children}</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CardBody({ children }: { children: React.ReactNode }) {
|
|
30
|
+
return <div className="card-body">{children}</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Usage
|
|
34
|
+
<Card>
|
|
35
|
+
<CardHeader>Title</CardHeader>
|
|
36
|
+
<CardBody>Content</CardBody>
|
|
37
|
+
</Card>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Compound Components
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
interface TabsContextValue {
|
|
44
|
+
activeTab: string
|
|
45
|
+
setActiveTab: (tab: string) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
|
49
|
+
|
|
50
|
+
export function Tabs({ children, defaultTab }: {
|
|
51
|
+
children: React.ReactNode
|
|
52
|
+
defaultTab: string
|
|
53
|
+
}) {
|
|
54
|
+
const [activeTab, setActiveTab] = useState(defaultTab)
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
58
|
+
{children}
|
|
59
|
+
</TabsContext.Provider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function TabList({ children }: { children: React.ReactNode }) {
|
|
64
|
+
return <div className="tab-list">{children}</div>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
|
68
|
+
const context = useContext(TabsContext)
|
|
69
|
+
if (!context) throw new Error('Tab must be used within Tabs')
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
className={context.activeTab === id ? 'active' : ''}
|
|
74
|
+
onClick={() => context.setActiveTab(id)}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</button>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Usage
|
|
82
|
+
<Tabs defaultTab="overview">
|
|
83
|
+
<TabList>
|
|
84
|
+
<Tab id="overview">Overview</Tab>
|
|
85
|
+
<Tab id="details">Details</Tab>
|
|
86
|
+
</TabList>
|
|
87
|
+
</Tabs>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Render Props Pattern
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface DataLoaderProps<T> {
|
|
94
|
+
url: string
|
|
95
|
+
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
|
99
|
+
const [data, setData] = useState<T | null>(null)
|
|
100
|
+
const [loading, setLoading] = useState(true)
|
|
101
|
+
const [error, setError] = useState<Error | null>(null)
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
fetch(url)
|
|
105
|
+
.then(res => res.json())
|
|
106
|
+
.then(setData)
|
|
107
|
+
.catch(setError)
|
|
108
|
+
.finally(() => setLoading(false))
|
|
109
|
+
}, [url])
|
|
110
|
+
|
|
111
|
+
return <>{children(data, loading, error)}</>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Usage
|
|
115
|
+
<DataLoader<Market[]> url="/api/markets">
|
|
116
|
+
{(markets, loading, error) => {
|
|
117
|
+
if (loading) return <Spinner />
|
|
118
|
+
if (error) return <Error error={error} />
|
|
119
|
+
return <MarketList markets={markets!} />
|
|
120
|
+
}}
|
|
121
|
+
</DataLoader>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom Hooks Patterns
|
|
125
|
+
|
|
126
|
+
### State Management Hook
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
export function useToggle(initialValue = false): [boolean, () => void] {
|
|
130
|
+
const [value, setValue] = useState(initialValue)
|
|
131
|
+
|
|
132
|
+
const toggle = useCallback(() => {
|
|
133
|
+
setValue(v => !v)
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
return [value, toggle]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Usage
|
|
140
|
+
const [isOpen, toggleOpen] = useToggle()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Async Data Fetching Hook
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
interface UseQueryOptions<T> {
|
|
147
|
+
onSuccess?: (data: T) => void
|
|
148
|
+
onError?: (error: Error) => void
|
|
149
|
+
enabled?: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function useQuery<T>(
|
|
153
|
+
key: string,
|
|
154
|
+
fetcher: () => Promise<T>,
|
|
155
|
+
options?: UseQueryOptions<T>
|
|
156
|
+
) {
|
|
157
|
+
const [data, setData] = useState<T | null>(null)
|
|
158
|
+
const [error, setError] = useState<Error | null>(null)
|
|
159
|
+
const [loading, setLoading] = useState(false)
|
|
160
|
+
|
|
161
|
+
const refetch = useCallback(async () => {
|
|
162
|
+
setLoading(true)
|
|
163
|
+
setError(null)
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const result = await fetcher()
|
|
167
|
+
setData(result)
|
|
168
|
+
options?.onSuccess?.(result)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const error = err as Error
|
|
171
|
+
setError(error)
|
|
172
|
+
options?.onError?.(error)
|
|
173
|
+
} finally {
|
|
174
|
+
setLoading(false)
|
|
175
|
+
}
|
|
176
|
+
}, [fetcher, options])
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (options?.enabled !== false) {
|
|
180
|
+
refetch()
|
|
181
|
+
}
|
|
182
|
+
}, [key, refetch, options?.enabled])
|
|
183
|
+
|
|
184
|
+
return { data, error, loading, refetch }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Usage
|
|
188
|
+
const { data: markets, loading, error, refetch } = useQuery(
|
|
189
|
+
'markets',
|
|
190
|
+
() => fetch('/api/markets').then(r => r.json()),
|
|
191
|
+
{
|
|
192
|
+
onSuccess: data => console.log('Fetched', data.length, 'markets'),
|
|
193
|
+
onError: err => console.error('Failed:', err)
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Debounce Hook
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
202
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const handler = setTimeout(() => {
|
|
206
|
+
setDebouncedValue(value)
|
|
207
|
+
}, delay)
|
|
208
|
+
|
|
209
|
+
return () => clearTimeout(handler)
|
|
210
|
+
}, [value, delay])
|
|
211
|
+
|
|
212
|
+
return debouncedValue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Usage
|
|
216
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
217
|
+
const debouncedQuery = useDebounce(searchQuery, 500)
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (debouncedQuery) {
|
|
221
|
+
performSearch(debouncedQuery)
|
|
222
|
+
}
|
|
223
|
+
}, [debouncedQuery])
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## State Management Patterns
|
|
227
|
+
|
|
228
|
+
### Context + Reducer Pattern
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
interface State {
|
|
232
|
+
markets: Market[]
|
|
233
|
+
selectedMarket: Market | null
|
|
234
|
+
loading: boolean
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type Action =
|
|
238
|
+
| { type: 'SET_MARKETS'; payload: Market[] }
|
|
239
|
+
| { type: 'SELECT_MARKET'; payload: Market }
|
|
240
|
+
| { type: 'SET_LOADING'; payload: boolean }
|
|
241
|
+
|
|
242
|
+
function reducer(state: State, action: Action): State {
|
|
243
|
+
switch (action.type) {
|
|
244
|
+
case 'SET_MARKETS':
|
|
245
|
+
return { ...state, markets: action.payload }
|
|
246
|
+
case 'SELECT_MARKET':
|
|
247
|
+
return { ...state, selectedMarket: action.payload }
|
|
248
|
+
case 'SET_LOADING':
|
|
249
|
+
return { ...state, loading: action.payload }
|
|
250
|
+
default:
|
|
251
|
+
return state
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const MarketContext = createContext<{
|
|
256
|
+
state: State
|
|
257
|
+
dispatch: Dispatch<Action>
|
|
258
|
+
} | undefined>(undefined)
|
|
259
|
+
|
|
260
|
+
export function MarketProvider({ children }: { children: React.ReactNode }) {
|
|
261
|
+
const [state, dispatch] = useReducer(reducer, {
|
|
262
|
+
markets: [],
|
|
263
|
+
selectedMarket: null,
|
|
264
|
+
loading: false
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<MarketContext.Provider value={{ state, dispatch }}>
|
|
269
|
+
{children}
|
|
270
|
+
</MarketContext.Provider>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function useMarkets() {
|
|
275
|
+
const context = useContext(MarketContext)
|
|
276
|
+
if (!context) throw new Error('useMarkets must be used within MarketProvider')
|
|
277
|
+
return context
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Performance Optimization
|
|
282
|
+
|
|
283
|
+
### Memoization
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// ✅ useMemo for expensive computations
|
|
287
|
+
const sortedMarkets = useMemo(() => {
|
|
288
|
+
return markets.sort((a, b) => b.volume - a.volume)
|
|
289
|
+
}, [markets])
|
|
290
|
+
|
|
291
|
+
// ✅ useCallback for functions passed to children
|
|
292
|
+
const handleSearch = useCallback((query: string) => {
|
|
293
|
+
setSearchQuery(query)
|
|
294
|
+
}, [])
|
|
295
|
+
|
|
296
|
+
// ✅ React.memo for pure components
|
|
297
|
+
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
|
|
298
|
+
return (
|
|
299
|
+
<div className="market-card">
|
|
300
|
+
<h3>{market.name}</h3>
|
|
301
|
+
<p>{market.description}</p>
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Code Splitting & Lazy Loading
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { lazy, Suspense } from 'react'
|
|
311
|
+
|
|
312
|
+
// ✅ Lazy load heavy components
|
|
313
|
+
const HeavyChart = lazy(() => import('./HeavyChart'))
|
|
314
|
+
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
|
|
315
|
+
|
|
316
|
+
export function Dashboard() {
|
|
317
|
+
return (
|
|
318
|
+
<div>
|
|
319
|
+
<Suspense fallback={<ChartSkeleton />}>
|
|
320
|
+
<HeavyChart data={data} />
|
|
321
|
+
</Suspense>
|
|
322
|
+
|
|
323
|
+
<Suspense fallback={null}>
|
|
324
|
+
<ThreeJsBackground />
|
|
325
|
+
</Suspense>
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Virtualization for Long Lists
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
335
|
+
|
|
336
|
+
export function VirtualMarketList({ markets }: { markets: Market[] }) {
|
|
337
|
+
const parentRef = useRef<HTMLDivElement>(null)
|
|
338
|
+
|
|
339
|
+
const virtualizer = useVirtualizer({
|
|
340
|
+
count: markets.length,
|
|
341
|
+
getScrollElement: () => parentRef.current,
|
|
342
|
+
estimateSize: () => 100, // Estimated row height
|
|
343
|
+
overscan: 5 // Extra items to render
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
|
|
348
|
+
<div
|
|
349
|
+
style={{
|
|
350
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
351
|
+
position: 'relative'
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{virtualizer.getVirtualItems().map(virtualRow => (
|
|
355
|
+
<div
|
|
356
|
+
key={virtualRow.index}
|
|
357
|
+
style={{
|
|
358
|
+
position: 'absolute',
|
|
359
|
+
top: 0,
|
|
360
|
+
left: 0,
|
|
361
|
+
width: '100%',
|
|
362
|
+
height: `${virtualRow.size}px`,
|
|
363
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
<MarketCard market={markets[virtualRow.index]} />
|
|
367
|
+
</div>
|
|
368
|
+
))}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Form Handling Patterns
|
|
376
|
+
|
|
377
|
+
### Controlled Form with Validation
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
interface FormData {
|
|
381
|
+
name: string
|
|
382
|
+
description: string
|
|
383
|
+
endDate: string
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface FormErrors {
|
|
387
|
+
name?: string
|
|
388
|
+
description?: string
|
|
389
|
+
endDate?: string
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function CreateMarketForm() {
|
|
393
|
+
const [formData, setFormData] = useState<FormData>({
|
|
394
|
+
name: '',
|
|
395
|
+
description: '',
|
|
396
|
+
endDate: ''
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const [errors, setErrors] = useState<FormErrors>({})
|
|
400
|
+
|
|
401
|
+
const validate = (): boolean => {
|
|
402
|
+
const newErrors: FormErrors = {}
|
|
403
|
+
|
|
404
|
+
if (!formData.name.trim()) {
|
|
405
|
+
newErrors.name = 'Name is required'
|
|
406
|
+
} else if (formData.name.length > 200) {
|
|
407
|
+
newErrors.name = 'Name must be under 200 characters'
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!formData.description.trim()) {
|
|
411
|
+
newErrors.description = 'Description is required'
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!formData.endDate) {
|
|
415
|
+
newErrors.endDate = 'End date is required'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
setErrors(newErrors)
|
|
419
|
+
return Object.keys(newErrors).length === 0
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
423
|
+
e.preventDefault()
|
|
424
|
+
|
|
425
|
+
if (!validate()) return
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
await createMarket(formData)
|
|
429
|
+
// Success handling
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// Error handling
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<form onSubmit={handleSubmit}>
|
|
437
|
+
<input
|
|
438
|
+
value={formData.name}
|
|
439
|
+
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
440
|
+
placeholder="Market name"
|
|
441
|
+
/>
|
|
442
|
+
{errors.name && <span className="error">{errors.name}</span>}
|
|
443
|
+
|
|
444
|
+
{/* Other fields */}
|
|
445
|
+
|
|
446
|
+
<button type="submit">Create Market</button>
|
|
447
|
+
</form>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Error Boundary Pattern
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
interface ErrorBoundaryState {
|
|
456
|
+
hasError: boolean
|
|
457
|
+
error: Error | null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export class ErrorBoundary extends React.Component<
|
|
461
|
+
{ children: React.ReactNode },
|
|
462
|
+
ErrorBoundaryState
|
|
463
|
+
> {
|
|
464
|
+
state: ErrorBoundaryState = {
|
|
465
|
+
hasError: false,
|
|
466
|
+
error: null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
470
|
+
return { hasError: true, error }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
474
|
+
console.error('Error boundary caught:', error, errorInfo)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
render() {
|
|
478
|
+
if (this.state.hasError) {
|
|
479
|
+
return (
|
|
480
|
+
<div className="error-fallback">
|
|
481
|
+
<h2>Something went wrong</h2>
|
|
482
|
+
<p>{this.state.error?.message}</p>
|
|
483
|
+
<button onClick={() => this.setState({ hasError: false })}>
|
|
484
|
+
Try again
|
|
485
|
+
</button>
|
|
486
|
+
</div>
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return this.props.children
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Usage
|
|
495
|
+
<ErrorBoundary>
|
|
496
|
+
<App />
|
|
497
|
+
</ErrorBoundary>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Animation Patterns
|
|
501
|
+
|
|
502
|
+
### Framer Motion Animations
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
506
|
+
|
|
507
|
+
// ✅ List animations
|
|
508
|
+
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
|
|
509
|
+
return (
|
|
510
|
+
<AnimatePresence>
|
|
511
|
+
{markets.map(market => (
|
|
512
|
+
<motion.div
|
|
513
|
+
key={market.id}
|
|
514
|
+
initial={{ opacity: 0, y: 20 }}
|
|
515
|
+
animate={{ opacity: 1, y: 0 }}
|
|
516
|
+
exit={{ opacity: 0, y: -20 }}
|
|
517
|
+
transition={{ duration: 0.3 }}
|
|
518
|
+
>
|
|
519
|
+
<MarketCard market={market} />
|
|
520
|
+
</motion.div>
|
|
521
|
+
))}
|
|
522
|
+
</AnimatePresence>
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ✅ Modal animations
|
|
527
|
+
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
528
|
+
return (
|
|
529
|
+
<AnimatePresence>
|
|
530
|
+
{isOpen && (
|
|
531
|
+
<>
|
|
532
|
+
<motion.div
|
|
533
|
+
className="modal-overlay"
|
|
534
|
+
initial={{ opacity: 0 }}
|
|
535
|
+
animate={{ opacity: 1 }}
|
|
536
|
+
exit={{ opacity: 0 }}
|
|
537
|
+
onClick={onClose}
|
|
538
|
+
/>
|
|
539
|
+
<motion.div
|
|
540
|
+
className="modal-content"
|
|
541
|
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
542
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
543
|
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
544
|
+
>
|
|
545
|
+
{children}
|
|
546
|
+
</motion.div>
|
|
547
|
+
</>
|
|
548
|
+
)}
|
|
549
|
+
</AnimatePresence>
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## Accessibility Patterns
|
|
555
|
+
|
|
556
|
+
### Keyboard Navigation
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
export function Dropdown({ options, onSelect }: DropdownProps) {
|
|
560
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
561
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
562
|
+
|
|
563
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
564
|
+
switch (e.key) {
|
|
565
|
+
case 'ArrowDown':
|
|
566
|
+
e.preventDefault()
|
|
567
|
+
setActiveIndex(i => Math.min(i + 1, options.length - 1))
|
|
568
|
+
break
|
|
569
|
+
case 'ArrowUp':
|
|
570
|
+
e.preventDefault()
|
|
571
|
+
setActiveIndex(i => Math.max(i - 1, 0))
|
|
572
|
+
break
|
|
573
|
+
case 'Enter':
|
|
574
|
+
e.preventDefault()
|
|
575
|
+
onSelect(options[activeIndex])
|
|
576
|
+
setIsOpen(false)
|
|
577
|
+
break
|
|
578
|
+
case 'Escape':
|
|
579
|
+
setIsOpen(false)
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<div
|
|
586
|
+
role="combobox"
|
|
587
|
+
aria-expanded={isOpen}
|
|
588
|
+
aria-haspopup="listbox"
|
|
589
|
+
onKeyDown={handleKeyDown}
|
|
590
|
+
>
|
|
591
|
+
{/* Dropdown implementation */}
|
|
592
|
+
</div>
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Focus Management
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
601
|
+
const modalRef = useRef<HTMLDivElement>(null)
|
|
602
|
+
const previousFocusRef = useRef<HTMLElement | null>(null)
|
|
603
|
+
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (isOpen) {
|
|
606
|
+
// Save currently focused element
|
|
607
|
+
previousFocusRef.current = document.activeElement as HTMLElement
|
|
608
|
+
|
|
609
|
+
// Focus modal
|
|
610
|
+
modalRef.current?.focus()
|
|
611
|
+
} else {
|
|
612
|
+
// Restore focus when closing
|
|
613
|
+
previousFocusRef.current?.focus()
|
|
614
|
+
}
|
|
615
|
+
}, [isOpen])
|
|
616
|
+
|
|
617
|
+
return isOpen ? (
|
|
618
|
+
<div
|
|
619
|
+
ref={modalRef}
|
|
620
|
+
role="dialog"
|
|
621
|
+
aria-modal="true"
|
|
622
|
+
tabIndex={-1}
|
|
623
|
+
onKeyDown={e => e.key === 'Escape' && onClose()}
|
|
624
|
+
>
|
|
625
|
+
{children}
|
|
626
|
+
</div>
|
|
627
|
+
) : null
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.
|