@fpkit/acss 1.0.0-beta.1 → 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/README.md +32 -0
- package/docs/README.md +325 -0
- package/docs/guides/accessibility.md +764 -0
- package/docs/guides/architecture.md +705 -0
- package/docs/guides/composition.md +688 -0
- package/docs/guides/css-variables.md +522 -0
- package/docs/guides/storybook.md +828 -0
- package/docs/guides/testing.md +817 -0
- package/docs/testing/focus-indicator-testing.md +437 -0
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/icons/icon.d.cts +32 -32
- package/libs/components/icons/icon.d.ts +32 -32
- package/libs/components/list/list.css +1 -1
- package/libs/components/list/list.min.css +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/package.json +4 -3
- package/src/components/README.mdx +1 -1
- package/src/components/buttons/button.scss +5 -0
- package/src/components/buttons/button.stories.tsx +8 -5
- package/src/components/cards/card.stories.tsx +1 -1
- package/src/components/details/details.stories.tsx +1 -1
- package/src/components/form/form.stories.tsx +1 -1
- package/src/components/form/input.stories.tsx +1 -1
- package/src/components/form/select.stories.tsx +1 -1
- package/src/components/heading/README.mdx +292 -0
- package/src/components/icons/icon.stories.tsx +1 -1
- package/src/components/list/list.scss +1 -1
- package/src/components/nav/nav.stories.tsx +1 -1
- package/src/components/ui.stories.tsx +53 -19
- package/src/docs/accessibility.mdx +484 -0
- package/src/docs/composition.mdx +549 -0
- package/src/docs/css-variables.mdx +380 -0
- package/src/docs/fpkit-developer.mdx +545 -0
- package/src/introduction.mdx +356 -0
- package/src/styles/buttons/button.css +4 -0
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/index.css +9 -3
- package/src/styles/index.css.map +1 -1
- package/src/styles/list/list.css +1 -1
- package/src/styles/utilities/_disabled.scss +5 -4
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
# Component Composition Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to build custom components by composing existing @fpkit/acss components, following React best practices for reusability and maintainability.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Why Composition?](#why-composition)
|
|
8
|
+
- [Composition vs Creation Decision Tree](#composition-vs-creation-decision-tree)
|
|
9
|
+
- [Common Composition Patterns](#common-composition-patterns)
|
|
10
|
+
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
|
|
11
|
+
- [Real-World Examples](#real-world-examples)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why Composition?
|
|
16
|
+
|
|
17
|
+
**Composition over duplication** is a core principle in React development. When building custom components using @fpkit/acss, you should prefer composing existing components rather than creating from scratch.
|
|
18
|
+
|
|
19
|
+
### Benefits
|
|
20
|
+
|
|
21
|
+
- ✅ **Consistency**: Reusing existing components ensures UI consistency across your application
|
|
22
|
+
- ✅ **Maintainability**: Bug fixes and improvements in fpkit components propagate to your composed components automatically
|
|
23
|
+
- ✅ **Reduced Code**: Less code to write, test, and maintain
|
|
24
|
+
- ✅ **Tested Components**: Leverage existing test coverage from fpkit
|
|
25
|
+
- ✅ **Faster Development**: Build complex UIs from proven primitives
|
|
26
|
+
- ✅ **Accessibility**: Inherit WCAG-compliant patterns from fpkit components
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Composition vs Creation Decision Tree
|
|
31
|
+
|
|
32
|
+
Use this decision tree to determine whether to compose, extend, or create new:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────┐
|
|
36
|
+
│ New Component Need: "ComponentName" │
|
|
37
|
+
└──────────────┬──────────────────────┘
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌──────────────────────┐
|
|
41
|
+
│ Does fpkit have a │ YES → Use fpkit component directly
|
|
42
|
+
│ component that meets │ Customize with CSS variables
|
|
43
|
+
│ the need exactly? │
|
|
44
|
+
└──────┬───────────────┘
|
|
45
|
+
│ NO
|
|
46
|
+
▼
|
|
47
|
+
┌──────────────────────┐
|
|
48
|
+
│ Can it be built by │ YES → Compose existing components
|
|
49
|
+
│ combining 2+ fpkit │ Import and combine
|
|
50
|
+
│ components? │
|
|
51
|
+
└──────┬───────────────┘
|
|
52
|
+
│ NO
|
|
53
|
+
▼
|
|
54
|
+
┌──────────────────────┐
|
|
55
|
+
│ Can I extend an │ YES → Wrap fpkit component
|
|
56
|
+
│ fpkit component with │ Add custom logic/styling
|
|
57
|
+
│ additional features? │
|
|
58
|
+
└──────┬───────────────┘
|
|
59
|
+
│ NO
|
|
60
|
+
▼
|
|
61
|
+
┌──────────────────────┐
|
|
62
|
+
│ Create custom │
|
|
63
|
+
│ component from │
|
|
64
|
+
│ scratch using fpkit │
|
|
65
|
+
│ styling patterns │
|
|
66
|
+
└─────────────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Common Composition Patterns
|
|
72
|
+
|
|
73
|
+
### Pattern 1: Container + Content
|
|
74
|
+
|
|
75
|
+
**When to use**: Wrapping fpkit components with additional structure or layout.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Badge, Button } from '@fpkit/acss'
|
|
79
|
+
|
|
80
|
+
export const StatusButton = ({ status, children, ...props }) => {
|
|
81
|
+
return (
|
|
82
|
+
<Button {...props}>
|
|
83
|
+
{children}
|
|
84
|
+
<Badge variant={status}>{status}</Badge>
|
|
85
|
+
</Button>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Usage
|
|
90
|
+
<StatusButton status="success">Complete</StatusButton>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Use Cases**: IconButton, TaggedCard, LabeledInput, NotificationButton
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### Pattern 2: Conditional Composition
|
|
98
|
+
|
|
99
|
+
**When to use**: Different component combinations based on props or state.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Alert, Modal } from '@fpkit/acss'
|
|
103
|
+
|
|
104
|
+
export const Notification = ({ variant, inline, children, onClose, ...props }) => {
|
|
105
|
+
if (inline) {
|
|
106
|
+
return (
|
|
107
|
+
<Alert variant={variant} onClose={onClose}>
|
|
108
|
+
{children}
|
|
109
|
+
</Alert>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Modal isOpen={props.isOpen} onClose={onClose}>
|
|
115
|
+
<Alert variant={variant}>{children}</Alert>
|
|
116
|
+
</Modal>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Usage
|
|
121
|
+
<Notification inline variant="success">Saved!</Notification>
|
|
122
|
+
<Notification isOpen={showModal} variant="error" onClose={handleClose}>
|
|
123
|
+
Error occurred
|
|
124
|
+
</Notification>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Use Cases**: ResponsiveNav (mobile menu vs desktop nav), AdaptiveDialog, ConditionalAlert
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Pattern 3: Enhanced Wrapper
|
|
132
|
+
|
|
133
|
+
**When to use**: Adding behavior/features around an existing fpkit component.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { Button } from '@fpkit/acss'
|
|
137
|
+
import { useState } from 'react'
|
|
138
|
+
|
|
139
|
+
export const LoadingButton = ({ loading, onClick, children, ...props }) => {
|
|
140
|
+
const [isLoading, setIsLoading] = useState(loading)
|
|
141
|
+
|
|
142
|
+
const handleClick = async (e) => {
|
|
143
|
+
setIsLoading(true)
|
|
144
|
+
try {
|
|
145
|
+
await onClick?.(e)
|
|
146
|
+
} finally {
|
|
147
|
+
setIsLoading(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Button {...props} disabled={isLoading || props.disabled} onClick={handleClick}>
|
|
153
|
+
{isLoading && <span aria-label="Loading">⏳</span>}
|
|
154
|
+
<span style={{ opacity: isLoading ? 0.6 : 1 }}>{children}</span>
|
|
155
|
+
</Button>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Usage
|
|
160
|
+
<LoadingButton onClick={handleSubmit}>Submit Form</LoadingButton>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Use Cases**: ConfirmButton, TooltipButton, LoadingButton, DebounceInput
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### Pattern 4: List of Components
|
|
168
|
+
|
|
169
|
+
**When to use**: Rendering multiple instances of the same fpkit component.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { Tag } from '@fpkit/acss'
|
|
173
|
+
|
|
174
|
+
export const TagList = ({ tags, onRemove, ...props }) => {
|
|
175
|
+
return (
|
|
176
|
+
<div className="tag-list" {...props}>
|
|
177
|
+
{tags.map((tag) => (
|
|
178
|
+
<Tag
|
|
179
|
+
key={tag.id}
|
|
180
|
+
onClose={onRemove ? () => onRemove(tag) : undefined}
|
|
181
|
+
>
|
|
182
|
+
{tag.label}
|
|
183
|
+
</Tag>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Usage
|
|
190
|
+
<TagList
|
|
191
|
+
tags={[
|
|
192
|
+
{ id: 1, label: 'React' },
|
|
193
|
+
{ id: 2, label: 'TypeScript' },
|
|
194
|
+
]}
|
|
195
|
+
onRemove={handleRemoveTag}
|
|
196
|
+
/>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Use Cases**: ButtonGroup, BadgeList, BreadcrumbTrail, PillGroup
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Pattern 5: Compound Component
|
|
204
|
+
|
|
205
|
+
**When to use**: Multiple related fpkit components that work together.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { Card, Button } from '@fpkit/acss'
|
|
209
|
+
|
|
210
|
+
export const ActionCard = ({ title, children, actions, ...props }) => {
|
|
211
|
+
return (
|
|
212
|
+
<Card {...props}>
|
|
213
|
+
<Card.Header>
|
|
214
|
+
<Card.Title>{title}</Card.Title>
|
|
215
|
+
</Card.Header>
|
|
216
|
+
<Card.Content>{children}</Card.Content>
|
|
217
|
+
{actions && (
|
|
218
|
+
<Card.Footer>
|
|
219
|
+
{actions.map((action, i) => (
|
|
220
|
+
<Button key={i} {...action} />
|
|
221
|
+
))}
|
|
222
|
+
</Card.Footer>
|
|
223
|
+
)}
|
|
224
|
+
</Card>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Usage
|
|
229
|
+
<ActionCard
|
|
230
|
+
title="Confirm Action"
|
|
231
|
+
actions={[
|
|
232
|
+
{ children: 'Cancel', variant: 'secondary', onClick: handleCancel },
|
|
233
|
+
{ children: 'Confirm', variant: 'primary', onClick: handleConfirm },
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
236
|
+
Are you sure you want to proceed?
|
|
237
|
+
</ActionCard>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Use Cases**: FormDialog, ArticleCard, ProductCard, SettingsSection
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Anti-Patterns to Avoid
|
|
245
|
+
|
|
246
|
+
### ❌ Anti-Pattern 1: Over-Composition
|
|
247
|
+
|
|
248
|
+
**Problem**: Too many nested layers make the component hard to understand and debug.
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// ❌ Bad: Too many wrappers
|
|
252
|
+
<OuterWrapper>
|
|
253
|
+
<MiddleContainer>
|
|
254
|
+
<InnerBox>
|
|
255
|
+
<ContentWrapper>
|
|
256
|
+
<Button>Click</Button>
|
|
257
|
+
</ContentWrapper>
|
|
258
|
+
</InnerBox>
|
|
259
|
+
</MiddleContainer>
|
|
260
|
+
</OuterWrapper>
|
|
261
|
+
|
|
262
|
+
// ✅ Good: Simplified structure
|
|
263
|
+
<Container>
|
|
264
|
+
<Button>Click</Button>
|
|
265
|
+
</Container>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Rule**: Keep composition depth ≤ 3 levels when possible.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### ❌ Anti-Pattern 2: Prop Drilling Through Composition
|
|
273
|
+
|
|
274
|
+
**Problem**: Passing props through multiple layers of composed components.
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
// ❌ Bad: Props passed through many layers
|
|
278
|
+
<Wrapper theme={theme} size={size} variant={variant}>
|
|
279
|
+
<Container theme={theme} size={size}>
|
|
280
|
+
<Button theme={theme} size={size} variant={variant} />
|
|
281
|
+
</Container>
|
|
282
|
+
</Wrapper>
|
|
283
|
+
|
|
284
|
+
// ✅ Good: Use context or reduce composition depth
|
|
285
|
+
const ThemeContext = createContext()
|
|
286
|
+
|
|
287
|
+
<ThemeProvider value={{ theme, size, variant }}>
|
|
288
|
+
<Wrapper>
|
|
289
|
+
<Container>
|
|
290
|
+
<Button />
|
|
291
|
+
</Container>
|
|
292
|
+
</Wrapper>
|
|
293
|
+
</ThemeProvider>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Rule**: If passing >3 props through >2 levels, consider context or refactoring.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### ❌ Anti-Pattern 3: Duplicating Instead of Composing
|
|
301
|
+
|
|
302
|
+
**Problem**: Copy-pasting component logic instead of reusing fpkit components.
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
// ❌ Bad: Duplicating Badge logic
|
|
306
|
+
export const Status = ({ variant, children }) => {
|
|
307
|
+
return (
|
|
308
|
+
<span className={`status status-${variant}`}>
|
|
309
|
+
{children}
|
|
310
|
+
</span>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ✅ Good: Reuse fpkit Badge component
|
|
315
|
+
import { Badge } from '@fpkit/acss'
|
|
316
|
+
|
|
317
|
+
export const Status = ({ variant, children }) => {
|
|
318
|
+
return <Badge variant={variant}>{children}</Badge>
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Rule**: If your code looks similar to an fpkit component, reuse that component instead.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### ❌ Anti-Pattern 4: Composing Incompatible Components
|
|
327
|
+
|
|
328
|
+
**Problem**: Forcing components together that create accessibility or semantic issues.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
// ❌ Bad: Button inside Link creates nested interactive elements (a11y violation)
|
|
332
|
+
import { Button, Link } from '@fpkit/acss'
|
|
333
|
+
|
|
334
|
+
<Link href="/page">
|
|
335
|
+
<Button>Click me</Button>
|
|
336
|
+
</Link>
|
|
337
|
+
|
|
338
|
+
// ✅ Good: Use Button with polymorphic 'as' prop
|
|
339
|
+
<Button as="a" href="/page">
|
|
340
|
+
Click me
|
|
341
|
+
</Button>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Rule**: Check component APIs for polymorphic props (`as`) and compatibility before composing.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Real-World Examples
|
|
349
|
+
|
|
350
|
+
### Example 1: AlertDialog (Composition)
|
|
351
|
+
|
|
352
|
+
**Need**: "Create an AlertDialog component that shows alerts in a modal"
|
|
353
|
+
|
|
354
|
+
**Analysis**:
|
|
355
|
+
- fpkit has `Alert` component ✓
|
|
356
|
+
- fpkit has `Dialog` component ✓
|
|
357
|
+
- Can be composed from both
|
|
358
|
+
|
|
359
|
+
**Implementation**:
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
import { Alert, Dialog } from '@fpkit/acss'
|
|
363
|
+
|
|
364
|
+
export const AlertDialog = ({ variant, title, children, ...dialogProps }) => {
|
|
365
|
+
return (
|
|
366
|
+
<Dialog {...dialogProps}>
|
|
367
|
+
<Alert variant={variant}>
|
|
368
|
+
{title && <strong>{title}</strong>}
|
|
369
|
+
{children}
|
|
370
|
+
</Alert>
|
|
371
|
+
</Dialog>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Usage
|
|
376
|
+
<AlertDialog
|
|
377
|
+
isOpen={showDialog}
|
|
378
|
+
onClose={handleClose}
|
|
379
|
+
variant="error"
|
|
380
|
+
title="Error"
|
|
381
|
+
>
|
|
382
|
+
An error occurred. Please try again.
|
|
383
|
+
</AlertDialog>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### Example 2: IconButton (Composition)
|
|
389
|
+
|
|
390
|
+
**Need**: "Create an IconButton component with text and icon"
|
|
391
|
+
|
|
392
|
+
**Analysis**:
|
|
393
|
+
- fpkit has `Button` component ✓
|
|
394
|
+
- Icons can be added as children ✓
|
|
395
|
+
- Can be composed
|
|
396
|
+
|
|
397
|
+
**Implementation**:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { Button } from '@fpkit/acss'
|
|
401
|
+
|
|
402
|
+
export const IconButton = ({ icon, children, iconPosition = 'left', ...props }) => {
|
|
403
|
+
return (
|
|
404
|
+
<Button {...props}>
|
|
405
|
+
{iconPosition === 'left' && <span className="icon">{icon}</span>}
|
|
406
|
+
{children}
|
|
407
|
+
{iconPosition === 'right' && <span className="icon">{icon}</span>}
|
|
408
|
+
</Button>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Usage
|
|
413
|
+
<IconButton icon="💾" variant="primary">
|
|
414
|
+
Save Changes
|
|
415
|
+
</IconButton>
|
|
416
|
+
|
|
417
|
+
<IconButton icon="→" iconPosition="right" variant="secondary">
|
|
418
|
+
Next
|
|
419
|
+
</IconButton>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### Example 3: TagInput (Compound Composition)
|
|
425
|
+
|
|
426
|
+
**Need**: "Create a TagInput component that allows adding/removing tags"
|
|
427
|
+
|
|
428
|
+
**Analysis**:
|
|
429
|
+
- fpkit has `Tag` component ✓
|
|
430
|
+
- Standard `input` element for text entry
|
|
431
|
+
- Complex interaction → compose with custom logic
|
|
432
|
+
|
|
433
|
+
**Implementation**:
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
import { Tag } from '@fpkit/acss'
|
|
437
|
+
import { useState } from 'react'
|
|
438
|
+
|
|
439
|
+
export const TagInput = ({ value = [], onChange, placeholder, ...props }) => {
|
|
440
|
+
const [inputValue, setInputValue] = useState('')
|
|
441
|
+
|
|
442
|
+
const addTag = () => {
|
|
443
|
+
if (inputValue.trim() && !value.includes(inputValue.trim())) {
|
|
444
|
+
onChange?.([...value, inputValue.trim()])
|
|
445
|
+
setInputValue('')
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const removeTag = (tagToRemove) => {
|
|
450
|
+
onChange?.(value.filter((tag) => tag !== tagToRemove))
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div className="tag-input" {...props}>
|
|
455
|
+
<div className="tag-list">
|
|
456
|
+
{value.map((tag) => (
|
|
457
|
+
<Tag key={tag} onClose={() => removeTag(tag)}>
|
|
458
|
+
{tag}
|
|
459
|
+
</Tag>
|
|
460
|
+
))}
|
|
461
|
+
</div>
|
|
462
|
+
<input
|
|
463
|
+
type="text"
|
|
464
|
+
value={inputValue}
|
|
465
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
466
|
+
onKeyDown={(e) => {
|
|
467
|
+
if (e.key === 'Enter') {
|
|
468
|
+
e.preventDefault()
|
|
469
|
+
addTag()
|
|
470
|
+
}
|
|
471
|
+
}}
|
|
472
|
+
placeholder={placeholder || 'Add tag...'}
|
|
473
|
+
/>
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Usage
|
|
479
|
+
<TagInput
|
|
480
|
+
value={tags}
|
|
481
|
+
onChange={setTags}
|
|
482
|
+
placeholder="Add technology..."
|
|
483
|
+
/>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### Example 4: ConfirmButton (Enhanced Wrapper)
|
|
489
|
+
|
|
490
|
+
**Need**: "Button that requires confirmation before executing action"
|
|
491
|
+
|
|
492
|
+
**Implementation**:
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
import { Button, Dialog } from '@fpkit/acss'
|
|
496
|
+
import { useState } from 'react'
|
|
497
|
+
|
|
498
|
+
export const ConfirmButton = ({
|
|
499
|
+
confirmTitle = 'Confirm Action',
|
|
500
|
+
confirmMessage = 'Are you sure?',
|
|
501
|
+
onConfirm,
|
|
502
|
+
children,
|
|
503
|
+
...props
|
|
504
|
+
}) => {
|
|
505
|
+
const [showConfirm, setShowConfirm] = useState(false)
|
|
506
|
+
|
|
507
|
+
const handleConfirm = () => {
|
|
508
|
+
setShowConfirm(false)
|
|
509
|
+
onConfirm?.()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<>
|
|
514
|
+
<Button {...props} onClick={() => setShowConfirm(true)}>
|
|
515
|
+
{children}
|
|
516
|
+
</Button>
|
|
517
|
+
|
|
518
|
+
<Dialog isOpen={showConfirm} onClose={() => setShowConfirm(false)}>
|
|
519
|
+
<h2>{confirmTitle}</h2>
|
|
520
|
+
<p>{confirmMessage}</p>
|
|
521
|
+
<div className="dialog-actions">
|
|
522
|
+
<Button variant="secondary" onClick={() => setShowConfirm(false)}>
|
|
523
|
+
Cancel
|
|
524
|
+
</Button>
|
|
525
|
+
<Button variant="primary" onClick={handleConfirm}>
|
|
526
|
+
Confirm
|
|
527
|
+
</Button>
|
|
528
|
+
</div>
|
|
529
|
+
</Dialog>
|
|
530
|
+
</>
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Usage
|
|
535
|
+
<ConfirmButton
|
|
536
|
+
variant="danger"
|
|
537
|
+
confirmTitle="Delete Account"
|
|
538
|
+
confirmMessage="This action cannot be undone. Are you sure?"
|
|
539
|
+
onConfirm={handleDeleteAccount}
|
|
540
|
+
>
|
|
541
|
+
Delete Account
|
|
542
|
+
</ConfirmButton>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Styling Composed Components
|
|
548
|
+
|
|
549
|
+
When composing fpkit components, you can customize styles using CSS variables:
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
import { Button, Badge } from '@fpkit/acss'
|
|
553
|
+
|
|
554
|
+
export const PriorityButton = ({ priority, children, ...props }) => {
|
|
555
|
+
return (
|
|
556
|
+
<Button
|
|
557
|
+
{...props}
|
|
558
|
+
style={{
|
|
559
|
+
'--btn-padding-inline': '2rem',
|
|
560
|
+
'--btn-gap': '0.75rem',
|
|
561
|
+
}}
|
|
562
|
+
>
|
|
563
|
+
{children}
|
|
564
|
+
<Badge
|
|
565
|
+
variant={priority === 'high' ? 'error' : 'default'}
|
|
566
|
+
style={{
|
|
567
|
+
'--badge-fs': '0.75rem',
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
{priority}
|
|
571
|
+
</Badge>
|
|
572
|
+
</Button>
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
See the [CSS Variables Guide](./css-variables.md) for complete customization options.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## TypeScript Support
|
|
582
|
+
|
|
583
|
+
fpkit components are fully typed. When composing, preserve type safety:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
import { Button, ButtonProps } from '@fpkit/acss'
|
|
587
|
+
import { ReactNode } from 'react'
|
|
588
|
+
|
|
589
|
+
interface LoadingButtonProps extends ButtonProps {
|
|
590
|
+
loading?: boolean
|
|
591
|
+
loadingText?: ReactNode
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export const LoadingButton = ({
|
|
595
|
+
loading,
|
|
596
|
+
loadingText = 'Loading...',
|
|
597
|
+
children,
|
|
598
|
+
...props
|
|
599
|
+
}: LoadingButtonProps) => {
|
|
600
|
+
return (
|
|
601
|
+
<Button {...props} disabled={loading || props.disabled}>
|
|
602
|
+
{loading ? loadingText : children}
|
|
603
|
+
</Button>
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## Testing Composed Components
|
|
611
|
+
|
|
612
|
+
When testing compositions, focus on integration rather than unit testing fpkit components (they're already tested):
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
import { render, screen } from '@testing-library/react'
|
|
616
|
+
import userEvent from '@testing-library/user-event'
|
|
617
|
+
import { TagInput } from './tag-input'
|
|
618
|
+
|
|
619
|
+
describe('TagInput', () => {
|
|
620
|
+
it('adds tag on Enter key', async () => {
|
|
621
|
+
const handleChange = vi.fn()
|
|
622
|
+
render(<TagInput value={[]} onChange={handleChange} />)
|
|
623
|
+
|
|
624
|
+
const input = screen.getByPlaceholderText('Add tag...')
|
|
625
|
+
await userEvent.type(input, 'React{Enter}')
|
|
626
|
+
|
|
627
|
+
expect(handleChange).toHaveBeenCalledWith(['React'])
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
it('removes tag on close', async () => {
|
|
631
|
+
const handleChange = vi.fn()
|
|
632
|
+
render(<TagInput value={['React']} onChange={handleChange} />)
|
|
633
|
+
|
|
634
|
+
const closeButton = screen.getByRole('button', { name: /close/i })
|
|
635
|
+
await userEvent.click(closeButton)
|
|
636
|
+
|
|
637
|
+
expect(handleChange).toHaveBeenCalledWith([])
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
See the [Testing Guide](./testing.md) for more patterns.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Guidelines Summary
|
|
647
|
+
|
|
648
|
+
| Scenario | Approach | Strategy |
|
|
649
|
+
|----------|----------|----------|
|
|
650
|
+
| Exact match exists | Use directly | Customize with CSS variables |
|
|
651
|
+
| 2+ components can be combined | Compose | Import and combine |
|
|
652
|
+
| Similar to existing | Wrap/extend | Add custom logic around fpkit component |
|
|
653
|
+
| Needs custom UI | Create from scratch | Follow fpkit styling patterns |
|
|
654
|
+
| Complex multi-part UI | Compound composition | Use multiple related components |
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Best Practices
|
|
659
|
+
|
|
660
|
+
### ✅ Do
|
|
661
|
+
|
|
662
|
+
- **Start with fpkit components** - Check what exists before building custom
|
|
663
|
+
- **Preserve accessibility** - Keep ARIA attributes and keyboard navigation from fpkit components
|
|
664
|
+
- **Use CSS variables** - Customize appearance without modifying component structure
|
|
665
|
+
- **Document composition** - Note which fpkit components are used in JSDoc comments
|
|
666
|
+
- **Test integration** - Focus tests on how composed parts work together
|
|
667
|
+
- **Export cleanly** - Re-export composed components from a single file
|
|
668
|
+
|
|
669
|
+
### ❌ Don't
|
|
670
|
+
|
|
671
|
+
- **Don't duplicate fpkit logic** - If it exists in fpkit, reuse it
|
|
672
|
+
- **Don't break accessibility** - Nested interactive elements, missing ARIA attributes
|
|
673
|
+
- **Don't over-compose** - Keep composition depth reasonable (≤3 levels)
|
|
674
|
+
- **Don't prop drill** - Use context or reduce composition depth
|
|
675
|
+
- **Don't ignore polymorphism** - Use `as` prop instead of wrapping
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## Next Steps
|
|
680
|
+
|
|
681
|
+
- **[CSS Variables Guide](./css-variables.md)** - Learn how to customize fpkit components
|
|
682
|
+
- **[Accessibility Guide](./accessibility.md)** - Ensure compositions remain accessible
|
|
683
|
+
- **[Architecture Guide](./architecture.md)** - Understand fpkit component patterns
|
|
684
|
+
- **[Testing Guide](./testing.md)** - Learn testing strategies for composed components
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
**Remember**: Composition is about smart reuse. Don't compose for the sake of it – compose when it creates clearer, more maintainable code that leverages the tested, accessible primitives from @fpkit/acss.
|