oxycode-skills 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/LICENSE +21 -0
- package/README.md +143 -0
- package/index.js +17 -0
- package/package.json +49 -0
- package/skills/anti-slop/README.md +106 -0
- package/skills/anti-slop/SKILL.md +311 -0
- package/skills/anti-slop/examples/before-after.md +380 -0
- package/skills/anti-slop/references/quality-rubric.md +313 -0
- package/skills/anti-slop/references/slop-patterns.md +433 -0
- package/skills/component-architect/README.md +186 -0
- package/skills/component-architect/SKILL.md +644 -0
- package/skills/component-architect/examples/component-patterns.md +569 -0
- package/skills/component-architect/references/atomic-design.md +516 -0
- package/skills/design-audit/README.md +114 -0
- package/skills/design-audit/SKILL.md +305 -0
- package/skills/design-audit/examples/audit-report.md +424 -0
- package/skills/design-audit/references/scoring-rubric.md +498 -0
- package/skills/design-md/README.md +106 -0
- package/skills/design-md/SKILL.md +262 -0
- package/skills/design-md/examples/bad-design.md +195 -0
- package/skills/design-md/examples/good-design.md +250 -0
- package/skills/design-md/references/design-md-spec.md +267 -0
- package/skills/design-md/scripts/validate.sh +134 -0
- package/skills/ui-builder/README.md +169 -0
- package/skills/ui-builder/SKILL.md +357 -0
- package/skills/ui-builder/examples/dashboard.md +323 -0
- package/skills/ui-builder/examples/landing-page.md +396 -0
- package/skills/ui-builder/references/component-patterns.md +396 -0
- package/skills/ui-builder/references/layout-system.md +423 -0
- package/skills/ui-builder/references/polish-checklist.md +221 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
# Component Architecture Patterns
|
|
2
|
+
|
|
3
|
+
## Pattern 1: Compound Components
|
|
4
|
+
|
|
5
|
+
### Problem
|
|
6
|
+
Configuration overload with too many props.
|
|
7
|
+
|
|
8
|
+
### Solution
|
|
9
|
+
Use compound components for complex UI.
|
|
10
|
+
|
|
11
|
+
### Example: Tabs
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// ❌ BAD: Configuration overload
|
|
15
|
+
<Tabs
|
|
16
|
+
items={[
|
|
17
|
+
{ label: 'Tab 1', content: 'Content 1' },
|
|
18
|
+
{ label: 'Tab 2', content: 'Content 2' },
|
|
19
|
+
]}
|
|
20
|
+
activeTab={activeTab}
|
|
21
|
+
onChange={setActiveTab}
|
|
22
|
+
variant="underline"
|
|
23
|
+
size="md"
|
|
24
|
+
fullWidth
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
// ✅ GOOD: Compound components
|
|
28
|
+
<Tabs value={activeTab} onChange={setActiveTab}>
|
|
29
|
+
<Tabs.List>
|
|
30
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
31
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
32
|
+
</Tabs.List>
|
|
33
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
34
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
35
|
+
</Tabs>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Implementation
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// components/tabs/Tabs.tsx
|
|
42
|
+
import { createContext, useContext, useState } from 'react'
|
|
43
|
+
|
|
44
|
+
interface TabsContextValue {
|
|
45
|
+
value: string
|
|
46
|
+
onChange: (value: string) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TabsContext = createContext<TabsContextValue | null>(null)
|
|
50
|
+
|
|
51
|
+
function useTabsContext() {
|
|
52
|
+
const context = useContext(TabsContext)
|
|
53
|
+
if (!context) {
|
|
54
|
+
throw new Error('Tabs components must be used within <Tabs>')
|
|
55
|
+
}
|
|
56
|
+
return context
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface TabsProps {
|
|
60
|
+
value: string
|
|
61
|
+
onChange: (value: string) => void
|
|
62
|
+
children: React.ReactNode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function Tabs({ value, onChange, children }: TabsProps) {
|
|
66
|
+
return (
|
|
67
|
+
<TabsContext.Provider value={{ value, onChange }}>
|
|
68
|
+
<div>{children}</div>
|
|
69
|
+
</TabsContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Tabs.List = function TabsList({ children }: { children: React.ReactNode }) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex border-b border-zinc-200" role="tablist">
|
|
76
|
+
{children}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Tabs.Trigger = function TabsTrigger({
|
|
82
|
+
value,
|
|
83
|
+
children
|
|
84
|
+
}: {
|
|
85
|
+
value: string
|
|
86
|
+
children: React.ReactNode
|
|
87
|
+
}) {
|
|
88
|
+
const { value: selectedValue, onChange } = useTabsContext()
|
|
89
|
+
const isSelected = selectedValue === value
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
role="tab"
|
|
94
|
+
aria-selected={isSelected}
|
|
95
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
96
|
+
isSelected
|
|
97
|
+
? 'border-zinc-900 text-zinc-900'
|
|
98
|
+
: 'border-transparent text-zinc-500 hover:text-zinc-700'
|
|
99
|
+
}`}
|
|
100
|
+
onClick={() => onChange(value)}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</button>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Tabs.Content = function TabsContent({
|
|
108
|
+
value,
|
|
109
|
+
children
|
|
110
|
+
}: {
|
|
111
|
+
value: string
|
|
112
|
+
children: React.ReactNode
|
|
113
|
+
}) {
|
|
114
|
+
const { value: selectedValue } = useTabsContext()
|
|
115
|
+
|
|
116
|
+
if (selectedValue !== value) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div role="tabpanel" className="py-4">
|
|
122
|
+
{children}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Pattern 2: Render Props
|
|
131
|
+
|
|
132
|
+
### Problem
|
|
133
|
+
Need to customize rendering of list items.
|
|
134
|
+
|
|
135
|
+
### Solution
|
|
136
|
+
Use render props for flexible rendering.
|
|
137
|
+
|
|
138
|
+
### Example: DataList
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// ❌ BAD: Limited customization
|
|
142
|
+
<DataList items={items} renderItem={renderItem} />
|
|
143
|
+
|
|
144
|
+
// ✅ GOOD: Render props
|
|
145
|
+
<DataList items={items}>
|
|
146
|
+
{(item) => (
|
|
147
|
+
<DataList.Item key={item.id}>
|
|
148
|
+
<DataList.Content>{item.name}</DataList.Content>
|
|
149
|
+
</DataList.Item>
|
|
150
|
+
)}
|
|
151
|
+
</DataList>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Implementation
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// components/data-list/DataList.tsx
|
|
158
|
+
interface DataListProps<T> {
|
|
159
|
+
items: T[]
|
|
160
|
+
children: (item: T) => React.ReactNode
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function DataList<T>({ items, children }: DataListProps<T>) {
|
|
164
|
+
return (
|
|
165
|
+
<div className="divide-y divide-zinc-200">
|
|
166
|
+
{items.map(children)}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
DataList.Item = function DataListItem({ children }: { children: React.ReactNode }) {
|
|
172
|
+
return <div className="py-4">{children}</div>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
DataList.Content = function DataListContent({ children }: { children: React.ReactNode }) {
|
|
176
|
+
return <div className="text-sm text-zinc-900">{children}</div>
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Pattern 3: Custom Hooks
|
|
183
|
+
|
|
184
|
+
### Problem
|
|
185
|
+
Logic is duplicated across components.
|
|
186
|
+
|
|
187
|
+
### Solution
|
|
188
|
+
Extract logic into custom hooks.
|
|
189
|
+
|
|
190
|
+
### Example: useToggle
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// ❌ BAD: Duplicated logic
|
|
194
|
+
function Component1() {
|
|
195
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
196
|
+
const toggle = () => setIsOpen(!isOpen)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function Component2() {
|
|
200
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
201
|
+
const toggle = () => setIsOpen(!isOpen)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ✅ GOOD: Custom hook
|
|
205
|
+
function useToggle(initialValue = false) {
|
|
206
|
+
const [value, setValue] = useState(initialValue)
|
|
207
|
+
const toggle = useCallback(() => setValue(v => !v), [])
|
|
208
|
+
const setTrue = useCallback(() => setValue(true), [])
|
|
209
|
+
const setFalse = useCallback(() => setValue(false), [])
|
|
210
|
+
|
|
211
|
+
return { value, toggle, setTrue, setFalse }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function Component1() {
|
|
215
|
+
const { value: isOpen, toggle } = useToggle()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Component2() {
|
|
219
|
+
const { value: isOpen, toggle } = useToggle()
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Example: useMediaQuery
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// hooks/useMediaQuery.ts
|
|
227
|
+
import { useState, useEffect } from 'react'
|
|
228
|
+
|
|
229
|
+
export function useMediaQuery(query: string) {
|
|
230
|
+
const [matches, setMatches] = useState(false)
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
const media = window.matchMedia(query)
|
|
234
|
+
setMatches(media.matches)
|
|
235
|
+
|
|
236
|
+
const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
|
|
237
|
+
media.addEventListener('change', listener)
|
|
238
|
+
|
|
239
|
+
return () => media.removeEventListener('change', listener)
|
|
240
|
+
}, [query])
|
|
241
|
+
|
|
242
|
+
return matches
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Usage
|
|
246
|
+
function Component() {
|
|
247
|
+
const isMobile = useMediaQuery('(max-width: 768px)')
|
|
248
|
+
|
|
249
|
+
return isMobile ? <MobileLayout /> : <DesktopLayout />
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Pattern 4: Higher-Order Components
|
|
256
|
+
|
|
257
|
+
### Problem
|
|
258
|
+
Need to add behavior to multiple components.
|
|
259
|
+
|
|
260
|
+
### Solution
|
|
261
|
+
Use higher-order components.
|
|
262
|
+
|
|
263
|
+
### Example: withLoading
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
// ❌ BAD: Duplicated loading logic
|
|
267
|
+
function Component1({ isLoading, data }) {
|
|
268
|
+
if (isLoading) return <Spinner />
|
|
269
|
+
return <div>{data}</div>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function Component2({ isLoading, data }) {
|
|
273
|
+
if (isLoading) return <Spinner />
|
|
274
|
+
return <div>{data}</div>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ✅ GOOD: HOC
|
|
278
|
+
function withLoading<P>(
|
|
279
|
+
WrappedComponent: React.ComponentType<P>
|
|
280
|
+
) {
|
|
281
|
+
return function WithLoadingComponent({
|
|
282
|
+
isLoading,
|
|
283
|
+
...props
|
|
284
|
+
}: P & { isLoading: boolean }) {
|
|
285
|
+
if (isLoading) return <Spinner />
|
|
286
|
+
return <WrappedComponent {...(props as P)} />
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const UserCardWithLoading = withLoading(UserCard)
|
|
291
|
+
|
|
292
|
+
// Usage
|
|
293
|
+
<UserCardWithLoading isLoading={loading} user={user} />
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Pattern 5: Context Providers
|
|
299
|
+
|
|
300
|
+
### Problem
|
|
301
|
+
Need to share state across many components.
|
|
302
|
+
|
|
303
|
+
### Solution
|
|
304
|
+
Use context providers.
|
|
305
|
+
|
|
306
|
+
### Example: ThemeProvider
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// ❌ BAD: Prop drilling
|
|
310
|
+
function App() {
|
|
311
|
+
const [theme, setTheme] = useState('light')
|
|
312
|
+
return <Layout theme={theme} setTheme={setTheme} />
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function Layout({ theme, setTheme }) {
|
|
316
|
+
return <Header theme={theme} setTheme={setTheme} />
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function Header({ theme, setTheme }) {
|
|
320
|
+
return <ThemeToggle theme={theme} setTheme={setTheme} />
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ✅ GOOD: Context provider
|
|
324
|
+
const ThemeContext = createContext<{
|
|
325
|
+
theme: string
|
|
326
|
+
setTheme: (theme: string) => void
|
|
327
|
+
} | null>(null)
|
|
328
|
+
|
|
329
|
+
function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
330
|
+
const [theme, setTheme] = useState('light')
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
334
|
+
{children}
|
|
335
|
+
</ThemeContext.Provider>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function useTheme() {
|
|
340
|
+
const context = useContext(ThemeContext)
|
|
341
|
+
if (!context) {
|
|
342
|
+
throw new Error('useTheme must be used within ThemeProvider')
|
|
343
|
+
}
|
|
344
|
+
return context
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Usage
|
|
348
|
+
function App() {
|
|
349
|
+
return (
|
|
350
|
+
<ThemeProvider>
|
|
351
|
+
<Layout />
|
|
352
|
+
</ThemeProvider>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function Header() {
|
|
357
|
+
const { theme, setTheme } = useTheme()
|
|
358
|
+
return <ThemeToggle theme={theme} setTheme={setTheme} />
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Pattern 6: Polymorphic Components
|
|
365
|
+
|
|
366
|
+
### Problem
|
|
367
|
+
Component should render different elements.
|
|
368
|
+
|
|
369
|
+
### Solution
|
|
370
|
+
Use polymorphic components with `as` prop.
|
|
371
|
+
|
|
372
|
+
### Example: Box
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
// ❌ BAD: Fixed element
|
|
376
|
+
function Box({ children }) {
|
|
377
|
+
return <div className="p-4">{children}</div>
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Usage
|
|
381
|
+
<Box as="section">Section</Box> // ❌ Can't do this
|
|
382
|
+
|
|
383
|
+
// ✅ GOOD: Polymorphic
|
|
384
|
+
interface BoxProps {
|
|
385
|
+
as?: React.ElementType
|
|
386
|
+
children: React.ReactNode
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function Box({ as: Component = 'div', children, ...props }: BoxProps) {
|
|
390
|
+
return <Component className="p-4" {...props}>{children}</Component>
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Usage
|
|
394
|
+
<Box as="section">Section</Box> // ✅ Renders <section>
|
|
395
|
+
<Box as="article">Article</Box> // ✅ Renders <article>
|
|
396
|
+
<Box>Div</Box> // ✅ Renders <div>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Pattern 7: Controlled/Uncontrolled
|
|
402
|
+
|
|
403
|
+
### Problem
|
|
404
|
+
Component needs to work both ways.
|
|
405
|
+
|
|
406
|
+
### Solution
|
|
407
|
+
Support both controlled and uncontrolled modes.
|
|
408
|
+
|
|
409
|
+
### Example: Input
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
interface InputProps {
|
|
413
|
+
value?: string
|
|
414
|
+
defaultValue?: string
|
|
415
|
+
onChange?: (value: string) => void
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function Input({ value, defaultValue, onChange, ...props }: InputProps) {
|
|
419
|
+
const [internalValue, setInternalValue] = useState(defaultValue || '')
|
|
420
|
+
|
|
421
|
+
const isControlled = value !== undefined
|
|
422
|
+
const currentValue = isControlled ? value : internalValue
|
|
423
|
+
|
|
424
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
425
|
+
const newValue = e.target.value
|
|
426
|
+
if (!isControlled) {
|
|
427
|
+
setInternalValue(newValue)
|
|
428
|
+
}
|
|
429
|
+
onChange?.(newValue)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return <input value={currentValue} onChange={handleChange} {...props} />
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Controlled usage
|
|
436
|
+
const [value, setValue] = useState('')
|
|
437
|
+
<Input value={value} onChange={setValue} />
|
|
438
|
+
|
|
439
|
+
// Uncontrolled usage
|
|
440
|
+
<Input defaultValue="initial" />
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Pattern 8: Compound Component with Reducer
|
|
446
|
+
|
|
447
|
+
### Problem
|
|
448
|
+
Complex state management in compound components.
|
|
449
|
+
|
|
450
|
+
### Solution
|
|
451
|
+
Use reducer for complex state.
|
|
452
|
+
|
|
453
|
+
### Example: Accordion
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
type AccordionAction =
|
|
457
|
+
| { type: 'TOGGLE'; value: string }
|
|
458
|
+
| { type: 'CLOSE_ALL' }
|
|
459
|
+
|
|
460
|
+
interface AccordionState {
|
|
461
|
+
openItems: Set<string>
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function accordionReducer(state: AccordionState, action: AccordionAction): AccordionState {
|
|
465
|
+
switch (action.type) {
|
|
466
|
+
case 'TOGGLE': {
|
|
467
|
+
const newOpenItems = new Set(state.openItems)
|
|
468
|
+
if (newOpenItems.has(action.value)) {
|
|
469
|
+
newOpenItems.delete(action.value)
|
|
470
|
+
} else {
|
|
471
|
+
newOpenItems.add(action.value)
|
|
472
|
+
}
|
|
473
|
+
return { openItems: newOpenItems }
|
|
474
|
+
}
|
|
475
|
+
case 'CLOSE_ALL':
|
|
476
|
+
return { openItems: new Set() }
|
|
477
|
+
default:
|
|
478
|
+
return state
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
interface AccordionContextValue {
|
|
483
|
+
state: AccordionState
|
|
484
|
+
dispatch: React.Dispatch<AccordionAction>
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null)
|
|
488
|
+
|
|
489
|
+
function Accordion({ children }: { children: React.ReactNode }) {
|
|
490
|
+
const [state, dispatch] = useReducer(accordionReducer, { openItems: new Set() })
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<AccordionContext.Provider value={{ state, dispatch }}>
|
|
494
|
+
<div className="divide-y divide-zinc-200">{children}</div>
|
|
495
|
+
</AccordionContext.Provider>
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
Accordion.Item = function AccordionItem({
|
|
500
|
+
value,
|
|
501
|
+
children
|
|
502
|
+
}: {
|
|
503
|
+
value: string
|
|
504
|
+
children: React.ReactNode
|
|
505
|
+
}) {
|
|
506
|
+
return <div>{children}</div>
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
Accordion.Trigger = function AccordionTrigger({
|
|
510
|
+
value,
|
|
511
|
+
children
|
|
512
|
+
}: {
|
|
513
|
+
value: string
|
|
514
|
+
children: React.ReactNode
|
|
515
|
+
}) {
|
|
516
|
+
const { state, dispatch } = useContext(AccordionContext)!
|
|
517
|
+
const isOpen = state.openItems.has(value)
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<button
|
|
521
|
+
className="w-full py-4 text-left flex items-center justify-between"
|
|
522
|
+
onClick={() => dispatch({ type: 'TOGGLE', value })}
|
|
523
|
+
>
|
|
524
|
+
{children}
|
|
525
|
+
<ChevronDown className={`w-5 h-5 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
526
|
+
</button>
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
Accordion.Content = function AccordionContent({
|
|
531
|
+
value,
|
|
532
|
+
children
|
|
533
|
+
}: {
|
|
534
|
+
value: string
|
|
535
|
+
children: React.ReactNode
|
|
536
|
+
}) {
|
|
537
|
+
const { state } = useContext(AccordionContext)!
|
|
538
|
+
const isOpen = state.openItems.has(value)
|
|
539
|
+
|
|
540
|
+
if (!isOpen) return null
|
|
541
|
+
|
|
542
|
+
return <div className="pb-4">{children}</div>
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Summary
|
|
549
|
+
|
|
550
|
+
### When to Use Each Pattern
|
|
551
|
+
|
|
552
|
+
| Pattern | Use When |
|
|
553
|
+
|---------|----------|
|
|
554
|
+
| Compound Components | Complex UI with multiple parts |
|
|
555
|
+
| Render Props | Need to customize rendering |
|
|
556
|
+
| Custom Hooks | Extract reusable logic |
|
|
557
|
+
| HOC | Add behavior to multiple components |
|
|
558
|
+
| Context Providers | Share state across many components |
|
|
559
|
+
| Polymorphic | Component should render different elements |
|
|
560
|
+
| Controlled/Uncontrolled | Component needs flexibility |
|
|
561
|
+
| Reducer | Complex state management |
|
|
562
|
+
|
|
563
|
+
### Best Practices
|
|
564
|
+
|
|
565
|
+
1. **Start simple** - Don't over-engineer
|
|
566
|
+
2. **Extract when duplicated** - If you copy, extract
|
|
567
|
+
3. **Document usage** - Show examples
|
|
568
|
+
4. **Type everything** - Use TypeScript
|
|
569
|
+
5. **Test thoroughly** - Unit + integration tests
|