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,644 @@
|
|
|
1
|
+
# component-architect
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Design scalable component architectures using atomic design patterns, compound components, and composition over configuration. Prevents 800-line components and teaches proper structure.
|
|
6
|
+
|
|
7
|
+
## Trigger
|
|
8
|
+
|
|
9
|
+
Use this skill when:
|
|
10
|
+
- User asks to design a component system
|
|
11
|
+
- User wants to refactor large components
|
|
12
|
+
- User says "architect this component" or "design a component library"
|
|
13
|
+
- Building a design system
|
|
14
|
+
- Components are getting too complex
|
|
15
|
+
|
|
16
|
+
## Instructions
|
|
17
|
+
|
|
18
|
+
### Step 1: Analyze Current State
|
|
19
|
+
|
|
20
|
+
Before designing, understand:
|
|
21
|
+
- Current component structure
|
|
22
|
+
- Pain points (too large, hard to maintain)
|
|
23
|
+
- Reuse patterns
|
|
24
|
+
- Dependencies
|
|
25
|
+
|
|
26
|
+
### Step 2: Apply Atomic Design
|
|
27
|
+
|
|
28
|
+
Structure components using atomic design:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/
|
|
32
|
+
├── components/
|
|
33
|
+
│ ├── atoms/ # Basic building blocks
|
|
34
|
+
│ │ ├── Button/
|
|
35
|
+
│ │ ├── Input/
|
|
36
|
+
│ │ ├── Badge/
|
|
37
|
+
│ │ └── Icon/
|
|
38
|
+
│ ├── molecules/ # Groups of atoms
|
|
39
|
+
│ │ ├── form-field/
|
|
40
|
+
│ │ ├── card/
|
|
41
|
+
│ │ └── nav-item/
|
|
42
|
+
│ ├── organisms/ # Complex components
|
|
43
|
+
│ │ ├── header/
|
|
44
|
+
│ │ ├── sidebar/
|
|
45
|
+
│ │ └── data-table/
|
|
46
|
+
│ ├── templates/ # Page layouts
|
|
47
|
+
│ │ ├── dashboard/
|
|
48
|
+
│ │ ├── landing/
|
|
49
|
+
│ │ └── auth/
|
|
50
|
+
│ └── pages/ # Specific instances
|
|
51
|
+
│ ├── home/
|
|
52
|
+
│ ├── settings/
|
|
53
|
+
│ └── profile/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Step 3: Design Compound Components
|
|
57
|
+
|
|
58
|
+
Use compound components for complex UI:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// ❌ BAD: Configuration overload
|
|
62
|
+
<Select
|
|
63
|
+
options={options}
|
|
64
|
+
value={value}
|
|
65
|
+
onChange={onChange}
|
|
66
|
+
placeholder="Select..."
|
|
67
|
+
isSearchable
|
|
68
|
+
isClearable
|
|
69
|
+
isDisabled
|
|
70
|
+
isLoading
|
|
71
|
+
formatOptionLabel={formatLabel}
|
|
72
|
+
formatGroupLabel={formatGroup}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
// ✅ GOOD: Compound components
|
|
76
|
+
<Select value={value} onChange={onChange}>
|
|
77
|
+
<Select.Trigger>
|
|
78
|
+
<Select.Value placeholder="Select..." />
|
|
79
|
+
<Select.Icon />
|
|
80
|
+
</Select.Trigger>
|
|
81
|
+
<Select.Content>
|
|
82
|
+
{options.map(option => (
|
|
83
|
+
<Select.Item key={option.value} value={option.value}>
|
|
84
|
+
{option.label}
|
|
85
|
+
</Select.Item>
|
|
86
|
+
))}
|
|
87
|
+
</Select.Content>
|
|
88
|
+
</Select>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step 4: Use Composition Pattern
|
|
92
|
+
|
|
93
|
+
Compose components instead of configuring:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// ❌ BAD: One giant component
|
|
97
|
+
<Card
|
|
98
|
+
title="Title"
|
|
99
|
+
description="Description"
|
|
100
|
+
image="/image.jpg"
|
|
101
|
+
actions={<Button>Action</Button>}
|
|
102
|
+
footer={<div>Footer</div>}
|
|
103
|
+
header={<div>Header</div>}
|
|
104
|
+
variant="outlined"
|
|
105
|
+
size="large"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
// ✅ GOOD: Composition
|
|
109
|
+
<Card>
|
|
110
|
+
<Card.Header>
|
|
111
|
+
<Card.Title>Title</Card.Title>
|
|
112
|
+
<Card.Description>Description</Card.Description>
|
|
113
|
+
</Card.Header>
|
|
114
|
+
<Card.Image src="/image.jpg" />
|
|
115
|
+
<Card.Content>
|
|
116
|
+
{/* Content here */}
|
|
117
|
+
</Card.Content>
|
|
118
|
+
<Card.Footer>
|
|
119
|
+
<Button>Action</Button>
|
|
120
|
+
</Card.Footer>
|
|
121
|
+
</Card>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Step 5: Separate Concerns
|
|
125
|
+
|
|
126
|
+
Split components by concern:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// ❌ BAD: Mixed concerns
|
|
130
|
+
function UserCard({ user }) {
|
|
131
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
132
|
+
const [formData, setFormData] = useState(user)
|
|
133
|
+
|
|
134
|
+
const handleSubmit = async () => {
|
|
135
|
+
await updateUser(formData)
|
|
136
|
+
setIsEditing(false)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
{isEditing ? (
|
|
142
|
+
<form onSubmit={handleSubmit}>
|
|
143
|
+
{/* Edit form */}
|
|
144
|
+
</form>
|
|
145
|
+
) : (
|
|
146
|
+
<div>
|
|
147
|
+
{/* Display user */}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ✅ GOOD: Separated concerns
|
|
155
|
+
function UserCard({ user }) {
|
|
156
|
+
return (
|
|
157
|
+
<Card>
|
|
158
|
+
<Card.Content>
|
|
159
|
+
<UserInfo user={user} />
|
|
160
|
+
</Card.Content>
|
|
161
|
+
<Card.Footer>
|
|
162
|
+
<UserActions user={user} />
|
|
163
|
+
</Card.Footer>
|
|
164
|
+
</Card>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function UserInfo({ user }) {
|
|
169
|
+
return <div>{/* Display user */}</div>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function UserActions({ user }) {
|
|
173
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
174
|
+
return <Button onClick={() => setIsEditing(true)}>Edit</Button>
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Step 6: Create Component Variants
|
|
179
|
+
|
|
180
|
+
Use variants instead of props:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// ❌ BAD: Too many props
|
|
184
|
+
<Button
|
|
185
|
+
primary
|
|
186
|
+
secondary
|
|
187
|
+
danger
|
|
188
|
+
outline
|
|
189
|
+
ghost
|
|
190
|
+
size="small"
|
|
191
|
+
size="medium"
|
|
192
|
+
size="large"
|
|
193
|
+
loading
|
|
194
|
+
disabled
|
|
195
|
+
icon={<Icon />}
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
// ✅ GOOD: Variants
|
|
199
|
+
<Button variant="primary">Primary</Button>
|
|
200
|
+
<Button variant="secondary">Secondary</Button>
|
|
201
|
+
<Button variant="danger">Danger</Button>
|
|
202
|
+
<Button variant="outline">Outline</Button>
|
|
203
|
+
<Button variant="ghost">Ghost</Button>
|
|
204
|
+
<Button size="sm">Small</Button>
|
|
205
|
+
<Button size="md">Medium</Button>
|
|
206
|
+
<Button size="lg">Large</Button>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Step 7: Extract Custom Hooks
|
|
210
|
+
|
|
211
|
+
Extract logic into custom hooks:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// ❌ BAD: Logic in component
|
|
215
|
+
function DataTable({ data }) {
|
|
216
|
+
const [sort, setSort] = useState('name')
|
|
217
|
+
const [filter, setFilter] = useState('')
|
|
218
|
+
const [page, setPage] = useState(1)
|
|
219
|
+
|
|
220
|
+
const filteredData = data.filter(item =>
|
|
221
|
+
item.name.includes(filter)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const sortedData = filteredData.sort((a, b) =>
|
|
225
|
+
a[sort].localeCompare(b[sort])
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const paginatedData = sortedData.slice(
|
|
229
|
+
(page - 1) * 10,
|
|
230
|
+
page * 10
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return <Table data={paginatedData} />
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ✅ GOOD: Custom hook
|
|
237
|
+
function useDataTable(data) {
|
|
238
|
+
const [sort, setSort] = useState('name')
|
|
239
|
+
const [filter, setFilter] = useState('')
|
|
240
|
+
const [page, setPage] = useState(1)
|
|
241
|
+
|
|
242
|
+
const filteredData = data.filter(item =>
|
|
243
|
+
item.name.includes(filter)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const sortedData = filteredData.sort((a, b) =>
|
|
247
|
+
a[sort].localeCompare(b[sort])
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const paginatedData = sortedData.slice(
|
|
251
|
+
(page - 1) * 10,
|
|
252
|
+
page * 10
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return { data: paginatedData, sort, setSort, filter, setFilter, page, setPage }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function DataTable({ data }) {
|
|
259
|
+
const { data: tableData } = useDataTable(data)
|
|
260
|
+
return <Table data={tableData} />
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Step 8: Document Components
|
|
265
|
+
|
|
266
|
+
Create clear documentation:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
/**
|
|
270
|
+
* Button component for user actions.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* <Button variant="primary" size="md">
|
|
274
|
+
* Click me
|
|
275
|
+
* </Button>
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* <Button variant="outline" isLoading>
|
|
279
|
+
* Loading
|
|
280
|
+
* </Button>
|
|
281
|
+
*/
|
|
282
|
+
interface ButtonProps {
|
|
283
|
+
/** Button variant */
|
|
284
|
+
variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost'
|
|
285
|
+
/** Button size */
|
|
286
|
+
size?: 'sm' | 'md' | 'lg'
|
|
287
|
+
/** Loading state */
|
|
288
|
+
isLoading?: boolean
|
|
289
|
+
/** Disabled state */
|
|
290
|
+
disabled?: boolean
|
|
291
|
+
/** Button content */
|
|
292
|
+
children: React.ReactNode
|
|
293
|
+
/** Click handler */
|
|
294
|
+
onClick?: () => void
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Component Patterns
|
|
299
|
+
|
|
300
|
+
### 1. Compound Components
|
|
301
|
+
```tsx
|
|
302
|
+
<Tabs>
|
|
303
|
+
<Tabs.List>
|
|
304
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
305
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
306
|
+
</Tabs.List>
|
|
307
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
308
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
309
|
+
</Tabs>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 2. Render Props
|
|
313
|
+
```tsx
|
|
314
|
+
<DataList items={items}>
|
|
315
|
+
{(item) => (
|
|
316
|
+
<DataList.Item key={item.id}>
|
|
317
|
+
<DataList.Content>{item.name}</DataList.Content>
|
|
318
|
+
</DataList.Item>
|
|
319
|
+
)}
|
|
320
|
+
</DataList>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 3. Higher-Order Components
|
|
324
|
+
```tsx
|
|
325
|
+
const withLoading = (WrappedComponent) => {
|
|
326
|
+
return function WithLoadingComponent({ isLoading, ...props }) {
|
|
327
|
+
if (isLoading) return <Spinner />
|
|
328
|
+
return <WrappedComponent {...props} />
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const UserCardWithLoading = withLoading(UserCard)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 4. Custom Hooks
|
|
336
|
+
```tsx
|
|
337
|
+
function useToggle(initialValue = false) {
|
|
338
|
+
const [value, setValue] = useState(initialValue)
|
|
339
|
+
const toggle = useCallback(() => setValue(v => !v), [])
|
|
340
|
+
return [value, toggle]
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 5. Context Providers
|
|
345
|
+
```tsx
|
|
346
|
+
const ThemeContext = createContext()
|
|
347
|
+
|
|
348
|
+
function ThemeProvider({ children }) {
|
|
349
|
+
const [theme, setTheme] = useState('light')
|
|
350
|
+
return (
|
|
351
|
+
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
352
|
+
{children}
|
|
353
|
+
</ThemeContext.Provider>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Anti-Patterns
|
|
359
|
+
|
|
360
|
+
### ❌ 800-Line Components
|
|
361
|
+
```tsx
|
|
362
|
+
// BAD: One giant component
|
|
363
|
+
function Dashboard() {
|
|
364
|
+
// 800 lines of code
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### ✅ Split Into Smaller Components
|
|
369
|
+
```tsx
|
|
370
|
+
// GOOD: Split by concern
|
|
371
|
+
function Dashboard() {
|
|
372
|
+
return (
|
|
373
|
+
<DashboardLayout>
|
|
374
|
+
<DashboardHeader />
|
|
375
|
+
<DashboardStats />
|
|
376
|
+
<DashboardTable />
|
|
377
|
+
<DashboardFooter />
|
|
378
|
+
</DashboardLayout>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### ❌ Configuration Overload
|
|
384
|
+
```tsx
|
|
385
|
+
// BAD: Too many props
|
|
386
|
+
<Component
|
|
387
|
+
prop1={value1}
|
|
388
|
+
prop2={value2}
|
|
389
|
+
prop3={value3}
|
|
390
|
+
prop4={value4}
|
|
391
|
+
prop5={value5}
|
|
392
|
+
prop6={value6}
|
|
393
|
+
prop7={value7}
|
|
394
|
+
prop8={value8}
|
|
395
|
+
/>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### ✅ Composition
|
|
399
|
+
```tsx
|
|
400
|
+
// GOOD: Compose components
|
|
401
|
+
<Component>
|
|
402
|
+
<Component.Header>Header</Component.Header>
|
|
403
|
+
<Component.Content>Content</Component.Content>
|
|
404
|
+
<Component.Footer>Footer</Component.Footer>
|
|
405
|
+
</Component>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### ❌ Mixed Concerns
|
|
409
|
+
```tsx
|
|
410
|
+
// BAD: UI + logic + data fetching
|
|
411
|
+
function UserCard() {
|
|
412
|
+
const [user, setUser] = useState(null)
|
|
413
|
+
const [loading, setLoading] = useState(true)
|
|
414
|
+
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
fetchUser().then(setUser)
|
|
417
|
+
}, [])
|
|
418
|
+
|
|
419
|
+
if (loading) return <Spinner />
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<div>
|
|
423
|
+
{/* UI + logic + data */}
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### ✅ Separated Concerns
|
|
430
|
+
```tsx
|
|
431
|
+
// GOOD: Separate UI, logic, data
|
|
432
|
+
function UserCard({ user }) {
|
|
433
|
+
return (
|
|
434
|
+
<Card>
|
|
435
|
+
<UserInfo user={user} />
|
|
436
|
+
<UserActions user={user} />
|
|
437
|
+
</Card>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function useUser(id) {
|
|
442
|
+
const [user, setUser] = useState(null)
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
fetchUser(id).then(setUser)
|
|
445
|
+
}, [id])
|
|
446
|
+
return user
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## File Structure
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
src/
|
|
454
|
+
├── components/
|
|
455
|
+
│ ├── atoms/
|
|
456
|
+
│ │ ├── Button/
|
|
457
|
+
│ │ │ ├── Button.tsx
|
|
458
|
+
│ │ │ ├── Button.test.tsx
|
|
459
|
+
│ │ │ ├── Button.stories.tsx
|
|
460
|
+
│ │ │ └── index.ts
|
|
461
|
+
│ │ ├── Input/
|
|
462
|
+
│ │ └── Badge/
|
|
463
|
+
│ ├── molecules/
|
|
464
|
+
│ │ ├── form-field/
|
|
465
|
+
│ │ └── card/
|
|
466
|
+
│ ├── organisms/
|
|
467
|
+
│ │ ├── header/
|
|
468
|
+
│ │ └── sidebar/
|
|
469
|
+
│ ├── templates/
|
|
470
|
+
│ └── pages/
|
|
471
|
+
├── hooks/
|
|
472
|
+
│ ├── useToggle.ts
|
|
473
|
+
│ ├── useDataTable.ts
|
|
474
|
+
│ └── useMediaQuery.ts
|
|
475
|
+
├── lib/
|
|
476
|
+
│ ├── utils.ts
|
|
477
|
+
│ └── cn.ts
|
|
478
|
+
└── styles/
|
|
479
|
+
└── globals.css
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Examples
|
|
483
|
+
|
|
484
|
+
### Example 1: Button Component
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
// atoms/Button/Button.tsx
|
|
488
|
+
import { cn } from '@/lib/utils'
|
|
489
|
+
|
|
490
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
491
|
+
variant?: 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost'
|
|
492
|
+
size?: 'sm' | 'md' | 'lg'
|
|
493
|
+
isLoading?: boolean
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const variants = {
|
|
497
|
+
primary: 'bg-zinc-900 text-white hover:bg-zinc-800',
|
|
498
|
+
secondary: 'bg-zinc-100 text-zinc-900 hover:bg-zinc-200',
|
|
499
|
+
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
500
|
+
outline: 'border border-zinc-300 text-zinc-700 hover:bg-zinc-50',
|
|
501
|
+
ghost: 'text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100',
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const sizes = {
|
|
505
|
+
sm: 'px-3 py-1.5 text-xs',
|
|
506
|
+
md: 'px-4 py-2 text-sm',
|
|
507
|
+
lg: 'px-6 py-3 text-base',
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function Button({
|
|
511
|
+
variant = 'primary',
|
|
512
|
+
size = 'md',
|
|
513
|
+
isLoading,
|
|
514
|
+
disabled,
|
|
515
|
+
className,
|
|
516
|
+
children,
|
|
517
|
+
...props
|
|
518
|
+
}: ButtonProps) {
|
|
519
|
+
return (
|
|
520
|
+
<button
|
|
521
|
+
className={cn(
|
|
522
|
+
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
|
|
523
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
524
|
+
variants[variant],
|
|
525
|
+
sizes[size],
|
|
526
|
+
className
|
|
527
|
+
)}
|
|
528
|
+
disabled={disabled || isLoading}
|
|
529
|
+
{...props}
|
|
530
|
+
>
|
|
531
|
+
{isLoading && <Spinner className="mr-2" />}
|
|
532
|
+
{children}
|
|
533
|
+
</button>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Example 2: Card Component
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
// molecules/Card/Card.tsx
|
|
542
|
+
import { cn } from '@/lib/utils'
|
|
543
|
+
|
|
544
|
+
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
545
|
+
|
|
546
|
+
export function Card({ className, children, ...props }: CardProps) {
|
|
547
|
+
return (
|
|
548
|
+
<div
|
|
549
|
+
className={cn(
|
|
550
|
+
'bg-white border border-zinc-200 rounded-lg',
|
|
551
|
+
className
|
|
552
|
+
)}
|
|
553
|
+
{...props}
|
|
554
|
+
>
|
|
555
|
+
{children}
|
|
556
|
+
</div>
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
Card.Header = function CardHeader({ className, children, ...props }) {
|
|
561
|
+
return (
|
|
562
|
+
<div
|
|
563
|
+
className={cn('px-6 py-4 border-b border-zinc-200', className)}
|
|
564
|
+
{...props}
|
|
565
|
+
>
|
|
566
|
+
{children}
|
|
567
|
+
</div>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
Card.Title = function CardTitle({ className, children, ...props }) {
|
|
572
|
+
return (
|
|
573
|
+
<h3
|
|
574
|
+
className={cn('text-lg font-semibold text-zinc-900', className)}
|
|
575
|
+
{...props}
|
|
576
|
+
>
|
|
577
|
+
{children}
|
|
578
|
+
</h3>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
Card.Content = function CardContent({ className, children, ...props }) {
|
|
583
|
+
return (
|
|
584
|
+
<div className={cn('px-6 py-4', className)} {...props}>
|
|
585
|
+
{children}
|
|
586
|
+
</div>
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
Card.Footer = function CardFooter({ className, children, ...props }) {
|
|
591
|
+
return (
|
|
592
|
+
<div
|
|
593
|
+
className={cn('px-6 py-4 border-t border-zinc-200', className)}
|
|
594
|
+
{...props}
|
|
595
|
+
>
|
|
596
|
+
{children}
|
|
597
|
+
</div>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Example 3: Custom Hook
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
// hooks/useToggle.ts
|
|
606
|
+
import { useState, useCallback } from 'react'
|
|
607
|
+
|
|
608
|
+
export function useToggle(initialValue = false) {
|
|
609
|
+
const [value, setValue] = useState(initialValue)
|
|
610
|
+
|
|
611
|
+
const toggle = useCallback(() => {
|
|
612
|
+
setValue(v => !v)
|
|
613
|
+
}, [])
|
|
614
|
+
|
|
615
|
+
const setTrue = useCallback(() => {
|
|
616
|
+
setValue(true)
|
|
617
|
+
}, [])
|
|
618
|
+
|
|
619
|
+
const setFalse = useCallback(() => {
|
|
620
|
+
setValue(false)
|
|
621
|
+
}, [])
|
|
622
|
+
|
|
623
|
+
return { value, toggle, setTrue, setFalse }
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## References
|
|
628
|
+
|
|
629
|
+
- [Atomic Design](https://atomicdesign.bradfrost.com/)
|
|
630
|
+
- [Compound Components](https://www.youtube.com/watch?v=v9ContrapKto)
|
|
631
|
+
- [React Patterns](https://reactpatterns.com/)
|
|
632
|
+
- [ui.shadcn.com](https://ui.shadcn.com/)
|
|
633
|
+
|
|
634
|
+
## Validation Checklist
|
|
635
|
+
|
|
636
|
+
Before delivering component architecture:
|
|
637
|
+
- [ ] Components follow atomic design
|
|
638
|
+
- [ ] No 800+ line components
|
|
639
|
+
- [ ] Compound components used appropriately
|
|
640
|
+
- [ ] Composition over configuration
|
|
641
|
+
- [ ] Concerns separated
|
|
642
|
+
- [ ] Custom hooks extracted
|
|
643
|
+
- [ ] TypeScript types defined
|
|
644
|
+
- [ ] Documentation provided
|