@discourser/design-system 0.3.0 → 0.4.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/guidelines/Guidelines.md +195 -0
- package/guidelines/components/accordion.md +639 -0
- package/guidelines/components/avatar.md +945 -0
- package/guidelines/components/badge.md +667 -0
- package/guidelines/components/button.md +314 -0
- package/guidelines/components/card.md +353 -0
- package/guidelines/components/checkbox.md +583 -0
- package/guidelines/components/dialog.md +465 -0
- package/guidelines/components/drawer.md +961 -0
- package/guidelines/components/heading.md +505 -0
- package/guidelines/components/icon-button.md +417 -0
- package/guidelines/components/input.md +499 -0
- package/guidelines/components/popover.md +1200 -0
- package/guidelines/components/progress.md +773 -0
- package/guidelines/components/radio-group.md +757 -0
- package/guidelines/components/select.md +1155 -0
- package/guidelines/components/skeleton.md +726 -0
- package/guidelines/components/switch.md +457 -0
- package/guidelines/components/tabs.md +834 -0
- package/guidelines/components/textarea.md +425 -0
- package/guidelines/components/toast.md +707 -0
- package/guidelines/components/tooltip.md +832 -0
- package/guidelines/design-tokens/colors.md +187 -0
- package/guidelines/design-tokens/elevation.md +274 -0
- package/guidelines/design-tokens/spacing.md +289 -0
- package/guidelines/design-tokens/typography.md +226 -0
- package/guidelines/overview-components.md +204 -0
- package/package.json +3 -2
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
**Purpose:** Primary interactive element for user actions following Material Design 3 patterns.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Button } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
The Button component supports 5 Material Design 3 variants, each with specific use cases:
|
|
14
|
+
|
|
15
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
16
|
+
|---------|-------------|-------|-------------|
|
|
17
|
+
| `filled` | Solid background with primary color | Primary actions | Submit forms, confirm dialogs, main CTAs |
|
|
18
|
+
| `outlined` | Transparent background with border | Secondary actions | Cancel buttons, back navigation, alternative options |
|
|
19
|
+
| `text` | Transparent background, no border | Tertiary actions | Links, less prominent actions, dialog actions |
|
|
20
|
+
| `elevated` | Elevated surface with subtle shadow | Floating actions | FAB-like buttons, actions that need emphasis but not primary color |
|
|
21
|
+
| `tonal` | Filled with secondary container color | Medium emphasis | Secondary CTAs, soft highlights, supportive actions |
|
|
22
|
+
|
|
23
|
+
### Visual Characteristics
|
|
24
|
+
|
|
25
|
+
- **filled**: Primary color background, white text, slight shadow on hover
|
|
26
|
+
- **outlined**: Transparent background, primary color text, 1px outline border
|
|
27
|
+
- **text**: Transparent background, primary color text, no border
|
|
28
|
+
- **elevated**: Surface container background, primary color text, level 1 shadow
|
|
29
|
+
- **tonal**: Secondary container background, on-secondary-container text
|
|
30
|
+
|
|
31
|
+
## Sizes
|
|
32
|
+
|
|
33
|
+
| Size | Height | Padding (Horizontal) | Font Size | Usage |
|
|
34
|
+
|------|--------|---------------------|-----------|-------|
|
|
35
|
+
| `sm` | 32px | 16px (md) | labelMedium | Compact UI, dense layouts, small dialogs |
|
|
36
|
+
| `md` | 40px | 24px (lg) | labelLarge | Default, most use cases |
|
|
37
|
+
| `lg` | 48px | 32px (xl) | labelLarge | Touch targets, mobile emphasis, hero sections |
|
|
38
|
+
|
|
39
|
+
**Recommendation:** Use `md` for most cases. Use `lg` for mobile-first designs or prominent CTAs.
|
|
40
|
+
|
|
41
|
+
## Props
|
|
42
|
+
|
|
43
|
+
| Prop | Type | Default | Description |
|
|
44
|
+
|------|------|---------|-------------|
|
|
45
|
+
| `variant` | `'filled' \| 'outlined' \| 'text' \| 'elevated' \| 'tonal'` | `'filled'` | Visual style variant |
|
|
46
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
|
|
47
|
+
| `leftIcon` | `ReactNode` | - | Icon or element before button text |
|
|
48
|
+
| `rightIcon` | `ReactNode` | - | Icon or element after button text |
|
|
49
|
+
| `disabled` | `boolean` | `false` | Disable button interaction |
|
|
50
|
+
| `onClick` | `(event: MouseEvent) => void` | - | Click handler |
|
|
51
|
+
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
|
|
52
|
+
| `className` | `string` | - | Additional CSS classes (use sparingly) |
|
|
53
|
+
| `children` | `ReactNode` | Required | Button text content |
|
|
54
|
+
|
|
55
|
+
**Note:** Button extends `ButtonHTMLAttributes<HTMLButtonElement>`, so all standard HTML button attributes are supported.
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### Basic Usage
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Primary action (default)
|
|
63
|
+
<Button>Submit</Button>
|
|
64
|
+
|
|
65
|
+
// Secondary action
|
|
66
|
+
<Button variant="outlined">Cancel</Button>
|
|
67
|
+
|
|
68
|
+
// Tertiary action
|
|
69
|
+
<Button variant="text">Learn More</Button>
|
|
70
|
+
|
|
71
|
+
// Medium emphasis
|
|
72
|
+
<Button variant="tonal">Save Draft</Button>
|
|
73
|
+
|
|
74
|
+
// Floating action
|
|
75
|
+
<Button variant="elevated">Create</Button>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### With Icons
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { PlusIcon, ArrowRightIcon } from 'your-icon-library';
|
|
82
|
+
|
|
83
|
+
// Icon on left
|
|
84
|
+
<Button leftIcon={<PlusIcon />}>
|
|
85
|
+
Add Item
|
|
86
|
+
</Button>
|
|
87
|
+
|
|
88
|
+
// Icon on right
|
|
89
|
+
<Button rightIcon={<ArrowRightIcon />}>
|
|
90
|
+
Continue
|
|
91
|
+
</Button>
|
|
92
|
+
|
|
93
|
+
// Both icons (rare, but supported)
|
|
94
|
+
<Button leftIcon={<PlusIcon />} rightIcon={<ArrowRightIcon />}>
|
|
95
|
+
Add and Continue
|
|
96
|
+
</Button>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Sizes
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Small button (compact UI)
|
|
103
|
+
<Button size="sm">Save</Button>
|
|
104
|
+
|
|
105
|
+
// Default size
|
|
106
|
+
<Button size="md">Submit</Button>
|
|
107
|
+
|
|
108
|
+
// Large button (mobile-friendly)
|
|
109
|
+
<Button size="lg">Get Started</Button>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Form Integration
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Submit button
|
|
116
|
+
<form onSubmit={handleSubmit}>
|
|
117
|
+
<Input label="Email" />
|
|
118
|
+
<Button type="submit">Sign Up</Button>
|
|
119
|
+
</form>
|
|
120
|
+
|
|
121
|
+
// Reset button
|
|
122
|
+
<Button type="reset" variant="text">Reset Form</Button>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Disabled State
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
129
|
+
|
|
130
|
+
<Button disabled={isSubmitting}>
|
|
131
|
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
132
|
+
</Button>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Event Handling
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const handleClick = () => {
|
|
139
|
+
console.log('Button clicked!');
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
<Button onClick={handleClick}>Click Me</Button>
|
|
143
|
+
|
|
144
|
+
// With event parameter
|
|
145
|
+
<Button onClick={(e) => {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
handleSubmit();
|
|
148
|
+
}}>
|
|
149
|
+
Submit
|
|
150
|
+
</Button>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Common Patterns
|
|
154
|
+
|
|
155
|
+
### Primary/Secondary Button Group
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
<div className={css({ display: 'flex', gap: 'sm' })}>
|
|
159
|
+
<Button variant="outlined">Cancel</Button>
|
|
160
|
+
<Button variant="filled">Confirm</Button>
|
|
161
|
+
</div>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Dialog Actions
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
<Dialog.Content>
|
|
168
|
+
<Dialog.Title>Confirm Action</Dialog.Title>
|
|
169
|
+
<Dialog.Description>Are you sure you want to proceed?</Dialog.Description>
|
|
170
|
+
|
|
171
|
+
<div className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
|
|
172
|
+
<Button variant="text">Cancel</Button>
|
|
173
|
+
<Button variant="filled">Confirm</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</Dialog.Content>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Loading State
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const [loading, setLoading] = useState(false);
|
|
182
|
+
|
|
183
|
+
<Button disabled={loading}>
|
|
184
|
+
{loading && <Spinner />}
|
|
185
|
+
{loading ? 'Loading...' : 'Submit'}
|
|
186
|
+
</Button>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## DO NOT
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// ❌ Don't use native button element
|
|
193
|
+
<button className="...">Submit</button> // Use <Button> instead
|
|
194
|
+
|
|
195
|
+
// ❌ Don't override button styles with inline styles
|
|
196
|
+
<Button style={{ backgroundColor: 'red' }}>Delete</Button>
|
|
197
|
+
|
|
198
|
+
// ❌ Don't use multiple filled buttons next to each other (unclear hierarchy)
|
|
199
|
+
<div>
|
|
200
|
+
<Button variant="filled">Save</Button>
|
|
201
|
+
<Button variant="filled">Delete</Button> // Use outlined or text instead
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
// ❌ Don't use button for navigation (use <a> tag or Next.js Link)
|
|
205
|
+
<Button onClick={() => router.push('/page')}>Go to Page</Button> // Bad
|
|
206
|
+
|
|
207
|
+
// ❌ Don't omit text for accessibility (use IconButton instead)
|
|
208
|
+
<Button><TrashIcon /></Button> // Use <IconButton> for icon-only
|
|
209
|
+
|
|
210
|
+
// ✅ Use IconButton for icon-only actions
|
|
211
|
+
<IconButton aria-label="Delete"><TrashIcon /></IconButton>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Accessibility
|
|
215
|
+
|
|
216
|
+
The Button component follows WCAG 2.1 Level AA standards:
|
|
217
|
+
|
|
218
|
+
- **Keyboard Navigation**: Focusable via Tab key, activates with Enter/Space
|
|
219
|
+
- **Focus Indicator**: 2px outline on focus-visible
|
|
220
|
+
- **Disabled State**: Uses `disabled` attribute, opacity 0.38, pointer-events: none
|
|
221
|
+
- **Touch Target**: Minimum 44x44px (use `md` or `lg` size)
|
|
222
|
+
- **Color Contrast**: All variants meet 4.5:1 contrast ratio
|
|
223
|
+
|
|
224
|
+
### Accessibility Best Practices
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// ✅ Always provide meaningful text
|
|
228
|
+
<Button>Submit Form</Button>
|
|
229
|
+
|
|
230
|
+
// ✅ Use aria-label for dynamic content
|
|
231
|
+
<Button aria-label={`Delete ${itemName}`}>Delete</Button>
|
|
232
|
+
|
|
233
|
+
// ✅ Indicate loading state
|
|
234
|
+
<Button aria-busy={loading} disabled={loading}>
|
|
235
|
+
{loading ? 'Loading...' : 'Submit'}
|
|
236
|
+
</Button>
|
|
237
|
+
|
|
238
|
+
// ✅ Use proper button types
|
|
239
|
+
<Button type="submit">Submit</Button>
|
|
240
|
+
<Button type="reset">Reset</Button>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Variant Selection Guide
|
|
244
|
+
|
|
245
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
246
|
+
|----------|-------------------|-----------|
|
|
247
|
+
| Form submission | `filled` | Primary action, needs highest emphasis |
|
|
248
|
+
| Cancel/Back | `outlined` or `text` | Secondary action, lower emphasis |
|
|
249
|
+
| Dialog confirmation | `filled` | Primary action in dialog |
|
|
250
|
+
| Dialog dismiss | `text` | Tertiary action, minimal emphasis |
|
|
251
|
+
| Save draft | `tonal` | Medium emphasis, not primary action |
|
|
252
|
+
| Delete/Destructive | `filled` or `tonal` | High attention, but consider error colors |
|
|
253
|
+
| Filter/Sort | `text` or `outlined` | Lower emphasis, frequent use |
|
|
254
|
+
| Floating action button | `elevated` | Needs to float above content |
|
|
255
|
+
| Link-like actions | `text` | Minimal emphasis, inline with text |
|
|
256
|
+
|
|
257
|
+
## State Behaviors
|
|
258
|
+
|
|
259
|
+
| State | Visual Change | Behavior |
|
|
260
|
+
|-------|---------------|----------|
|
|
261
|
+
| **Hover** | Opacity change or shadow | `filled`: 92% opacity + level1 shadow<br />`outlined`: 8% primary background<br />`elevated`: Increase shadow to level2 |
|
|
262
|
+
| **Active** | Further opacity/shadow change | `filled`: 88% opacity<br />`outlined`: 12% primary background |
|
|
263
|
+
| **Focus** | 2px outline | Primary color outline, 2px offset |
|
|
264
|
+
| **Disabled** | 38% opacity, no interaction | Cannot be clicked, greyed out appearance |
|
|
265
|
+
|
|
266
|
+
## Responsive Considerations
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Mobile-first: Use larger buttons for touch
|
|
270
|
+
<Button size="lg">Submit</Button>
|
|
271
|
+
|
|
272
|
+
// Desktop: Can use smaller sizes
|
|
273
|
+
<Button size={{ base: 'lg', md: 'md' }}>Submit</Button>
|
|
274
|
+
|
|
275
|
+
// Responsive button group
|
|
276
|
+
<div className={css({
|
|
277
|
+
display: 'flex',
|
|
278
|
+
flexDirection: { base: 'column', md: 'row' },
|
|
279
|
+
gap: 'sm'
|
|
280
|
+
})}>
|
|
281
|
+
<Button variant="outlined">Cancel</Button>
|
|
282
|
+
<Button variant="filled">Confirm</Button>
|
|
283
|
+
</div>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Testing
|
|
287
|
+
|
|
288
|
+
When testing Button components:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { render, screen } from '@testing-library/react';
|
|
292
|
+
import userEvent from '@testing-library/user-event';
|
|
293
|
+
|
|
294
|
+
test('button handles click events', async () => {
|
|
295
|
+
const handleClick = vi.fn();
|
|
296
|
+
render(<Button onClick={handleClick}>Click Me</Button>);
|
|
297
|
+
|
|
298
|
+
const button = screen.getByRole('button', { name: 'Click Me' });
|
|
299
|
+
await userEvent.click(button);
|
|
300
|
+
|
|
301
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('disabled button cannot be clicked', async () => {
|
|
305
|
+
const handleClick = vi.fn();
|
|
306
|
+
render(<Button disabled onClick={handleClick}>Click Me</Button>);
|
|
307
|
+
|
|
308
|
+
const button = screen.getByRole('button', { name: 'Click Me' });
|
|
309
|
+
await userEvent.click(button);
|
|
310
|
+
|
|
311
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
312
|
+
expect(button).toBeDisabled();
|
|
313
|
+
});
|
|
314
|
+
```
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
**Purpose:** Container component for grouping related content with Material Design 3 elevation and styling.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Card } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
The Card component supports 3 Material Design 3 variants:
|
|
14
|
+
|
|
15
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
16
|
+
|---------|-------------|-------|-------------|
|
|
17
|
+
| `elevated` | Surface with shadow, elevated appearance | Default cards, content containers | Most common use case, provides visual hierarchy |
|
|
18
|
+
| `filled` | Filled background, no shadow | Secondary cards, less emphasis | When multiple cards are stacked, alternative style |
|
|
19
|
+
| `outlined` | Outlined border, no shadow | Tertiary cards, minimal style | When you want subtle separation without elevation |
|
|
20
|
+
|
|
21
|
+
### Visual Characteristics
|
|
22
|
+
|
|
23
|
+
- **elevated**: `surfaceContainerLow` background, level1 shadow, level2 shadow on hover
|
|
24
|
+
- **filled**: `surfaceContainerHighest` background, no shadow
|
|
25
|
+
- **outlined**: `surface` background, 1px `outlineVariant` border
|
|
26
|
+
|
|
27
|
+
## Props
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Description |
|
|
30
|
+
|------|------|---------|-------------|
|
|
31
|
+
| `variant` | `'elevated' \| 'filled' \| 'outlined'` | `'elevated'` | Visual style variant |
|
|
32
|
+
| `interactive` | `boolean` | `false` | Makes card clickable with hover/active states |
|
|
33
|
+
| `onClick` | `(event: MouseEvent) => void` | - | Click handler (sets interactive=true automatically) |
|
|
34
|
+
| `className` | `string` | - | Additional CSS classes (use sparingly) |
|
|
35
|
+
| `children` | `ReactNode` | Required | Card content |
|
|
36
|
+
|
|
37
|
+
**Note:** Card extends `HTMLAttributes<HTMLDivElement>`, so all standard HTML div attributes are supported.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Basic Usage
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Elevated card (default)
|
|
45
|
+
<Card>
|
|
46
|
+
<h3>Card Title</h3>
|
|
47
|
+
<p>Card content goes here...</p>
|
|
48
|
+
</Card>
|
|
49
|
+
|
|
50
|
+
// Filled card
|
|
51
|
+
<Card variant="filled">
|
|
52
|
+
<h3>Secondary Card</h3>
|
|
53
|
+
<p>Less emphasized content</p>
|
|
54
|
+
</Card>
|
|
55
|
+
|
|
56
|
+
// Outlined card
|
|
57
|
+
<Card variant="outlined">
|
|
58
|
+
<h3>Minimal Card</h3>
|
|
59
|
+
<p>Subtle separation</p>
|
|
60
|
+
</Card>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Interactive Cards
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// Clickable card
|
|
67
|
+
<Card interactive onClick={() => navigate('/details')}>
|
|
68
|
+
<h3>Click me!</h3>
|
|
69
|
+
<p>This card responds to clicks</p>
|
|
70
|
+
</Card>
|
|
71
|
+
|
|
72
|
+
// Card as link wrapper
|
|
73
|
+
<Card interactive as="a" href="/product">
|
|
74
|
+
<h3>Product Name</h3>
|
|
75
|
+
<p>Product description</p>
|
|
76
|
+
</Card>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### With Custom Styling
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { css } from '@discourser/design-system/styled-system/css';
|
|
83
|
+
|
|
84
|
+
// Card with custom padding
|
|
85
|
+
<Card className={css({ p: 'xl' })}>
|
|
86
|
+
<h3>Spacious Card</h3>
|
|
87
|
+
<p>Extra padding for comfort</p>
|
|
88
|
+
</Card>
|
|
89
|
+
|
|
90
|
+
// Card with custom width
|
|
91
|
+
<Card className={css({ width: '400px' })}>
|
|
92
|
+
<h3>Fixed Width Card</h3>
|
|
93
|
+
</Card>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Common Patterns
|
|
97
|
+
|
|
98
|
+
### Card Grid Layout
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { css } from '@discourser/design-system/styled-system/css';
|
|
102
|
+
|
|
103
|
+
const gridContainer = css({
|
|
104
|
+
display: 'grid',
|
|
105
|
+
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
|
|
106
|
+
gap: 'lg'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
<div className={gridContainer}>
|
|
110
|
+
<Card>
|
|
111
|
+
<h3>Card 1</h3>
|
|
112
|
+
<p>Content 1</p>
|
|
113
|
+
</Card>
|
|
114
|
+
<Card>
|
|
115
|
+
<h3>Card 2</h3>
|
|
116
|
+
<p>Content 2</p>
|
|
117
|
+
</Card>
|
|
118
|
+
<Card>
|
|
119
|
+
<h3>Card 3</h3>
|
|
120
|
+
<p>Content 3</p>
|
|
121
|
+
</Card>
|
|
122
|
+
</div>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Card with Image
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
<Card variant="elevated">
|
|
129
|
+
<img
|
|
130
|
+
src="/image.jpg"
|
|
131
|
+
alt="Description"
|
|
132
|
+
className={css({ width: '100%', height: '200px', objectFit: 'cover' })}
|
|
133
|
+
/>
|
|
134
|
+
<div className={css({ p: 'lg' })}>
|
|
135
|
+
<h3 className={css({ textStyle: 'titleLarge', mb: 'sm' })}>
|
|
136
|
+
Card Title
|
|
137
|
+
</h3>
|
|
138
|
+
<p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
|
|
139
|
+
Card description goes here with supporting details.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</Card>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Card with Actions
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
<Card>
|
|
149
|
+
<div className={css({ p: 'lg' })}>
|
|
150
|
+
<h3 className={css({ textStyle: 'titleLarge', mb: 'sm' })}>
|
|
151
|
+
Confirm Action
|
|
152
|
+
</h3>
|
|
153
|
+
<p className={css({ textStyle: 'bodyMedium', mb: 'md', color: 'onSurfaceVariant' })}>
|
|
154
|
+
Are you sure you want to proceed with this action?
|
|
155
|
+
</p>
|
|
156
|
+
<div className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
|
|
157
|
+
<Button variant="text">Cancel</Button>
|
|
158
|
+
<Button variant="filled">Confirm</Button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</Card>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### List of Cards
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const items = [
|
|
168
|
+
{ id: 1, title: 'Item 1', description: 'Description 1' },
|
|
169
|
+
{ id: 2, title: 'Item 2', description: 'Description 2' },
|
|
170
|
+
{ id: 3, title: 'Item 3', description: 'Description 3' },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
|
|
174
|
+
{items.map(item => (
|
|
175
|
+
<Card key={item.id} interactive onClick={() => handleClick(item.id)}>
|
|
176
|
+
<div className={css({ p: 'lg' })}>
|
|
177
|
+
<h3 className={css({ textStyle: 'titleMedium' })}>{item.title}</h3>
|
|
178
|
+
<p className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
|
|
179
|
+
{item.description}
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
</Card>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## DO NOT
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// ❌ Don't use div when you need card styling
|
|
191
|
+
<div className={css({ bg: 'surface', borderRadius: 'md', p: 'lg' })}>
|
|
192
|
+
Content
|
|
193
|
+
</div> // Use <Card> instead
|
|
194
|
+
|
|
195
|
+
// ❌ Don't override elevation styles
|
|
196
|
+
<Card style={{ boxShadow: '0 10px 50px rgba(0,0,0,0.5)' }}>
|
|
197
|
+
Content
|
|
198
|
+
</Card>
|
|
199
|
+
|
|
200
|
+
// ❌ Don't nest cards inappropriately
|
|
201
|
+
<Card>
|
|
202
|
+
<Card>Nested card</Card> // Avoid nesting cards
|
|
203
|
+
</Card>
|
|
204
|
+
|
|
205
|
+
// ❌ Don't use card for everything (over-cardification)
|
|
206
|
+
<Card>
|
|
207
|
+
<Card>Login</Card>
|
|
208
|
+
<Card>Signup</Card>
|
|
209
|
+
</Card> // Consider simpler layouts
|
|
210
|
+
|
|
211
|
+
// ✅ Use cards for logical content grouping
|
|
212
|
+
<div className={css({ display: 'flex', gap: 'md' })}>
|
|
213
|
+
<Card>Login Form</Card>
|
|
214
|
+
<Card>Signup Form</Card>
|
|
215
|
+
</div>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Accessibility
|
|
219
|
+
|
|
220
|
+
The Card component follows accessibility best practices:
|
|
221
|
+
|
|
222
|
+
- **Semantic HTML**: Uses `<div>` by default, appropriate for content containers
|
|
223
|
+
- **Interactive State**: When `interactive={true}`, card has proper cursor and hover states
|
|
224
|
+
- **Keyboard Navigation**: If used as link/button, ensure proper tabindex and keyboard support
|
|
225
|
+
- **Focus Indicators**: Should add focus styles when interactive
|
|
226
|
+
|
|
227
|
+
### Accessibility Best Practices
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// ✅ Use semantic elements for clickable cards
|
|
231
|
+
<Card as="button" interactive onClick={handleClick} aria-label="View details">
|
|
232
|
+
Card content
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
// ✅ Use proper heading hierarchy
|
|
236
|
+
<Card>
|
|
237
|
+
<h2 className={css({ textStyle: 'titleLarge' })}>Card Title</h2>
|
|
238
|
+
<p>Content</p>
|
|
239
|
+
</Card>
|
|
240
|
+
|
|
241
|
+
// ✅ Ensure sufficient color contrast
|
|
242
|
+
<Card>
|
|
243
|
+
<p className={css({ color: 'onSurface' })}>Primary text</p>
|
|
244
|
+
<p className={css({ color: 'onSurfaceVariant' })}>Secondary text</p>
|
|
245
|
+
</Card>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Variant Selection Guide
|
|
249
|
+
|
|
250
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
251
|
+
|----------|-------------------|-----------|
|
|
252
|
+
| Product cards | `elevated` | Visual hierarchy, draws attention |
|
|
253
|
+
| Form sections | `outlined` | Subtle separation without heavy elevation |
|
|
254
|
+
| Dashboard widgets | `elevated` | Emphasizes different data sections |
|
|
255
|
+
| List items | `outlined` or `filled` | Lighter style for repeated elements |
|
|
256
|
+
| Content previews | `elevated` | Interactive, prominent |
|
|
257
|
+
| Settings sections | `outlined` | Clean, minimal separation |
|
|
258
|
+
|
|
259
|
+
## State Behaviors
|
|
260
|
+
|
|
261
|
+
| State | Visual Change | Applies When |
|
|
262
|
+
|-------|---------------|--------------|
|
|
263
|
+
| **Hover** | `elevated`: shadow increases to level2<br />`filled`/`outlined`: slight opacity change | Only when `interactive={true}` |
|
|
264
|
+
| **Active** | Opacity reduces to 0.92 | Only when `interactive={true}` |
|
|
265
|
+
| **Default** | No hover effects | When `interactive={false}` (default) |
|
|
266
|
+
|
|
267
|
+
## Responsive Considerations
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Responsive card width
|
|
271
|
+
<Card className={css({
|
|
272
|
+
width: { base: '100%', md: '400px' }
|
|
273
|
+
})}>
|
|
274
|
+
Content
|
|
275
|
+
</Card>
|
|
276
|
+
|
|
277
|
+
// Responsive padding
|
|
278
|
+
<Card>
|
|
279
|
+
<div className={css({ p: { base: 'md', lg: 'xl' } })}>
|
|
280
|
+
Content with responsive padding
|
|
281
|
+
</div>
|
|
282
|
+
</Card>
|
|
283
|
+
|
|
284
|
+
// Responsive grid
|
|
285
|
+
const gridStyles = css({
|
|
286
|
+
display: 'grid',
|
|
287
|
+
gridTemplateColumns: {
|
|
288
|
+
base: '1fr',
|
|
289
|
+
sm: 'repeat(2, 1fr)',
|
|
290
|
+
lg: 'repeat(3, 1fr)',
|
|
291
|
+
xl: 'repeat(4, 1fr)'
|
|
292
|
+
},
|
|
293
|
+
gap: 'lg'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
<div className={gridStyles}>
|
|
297
|
+
{items.map(item => <Card key={item.id}>...</Card>)}
|
|
298
|
+
</div>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Content Padding
|
|
302
|
+
|
|
303
|
+
Cards have no default padding. Add padding to card content:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// ✅ Recommended: Add padding to content
|
|
307
|
+
<Card>
|
|
308
|
+
<div className={css({ p: 'lg' })}>
|
|
309
|
+
Content with padding
|
|
310
|
+
</div>
|
|
311
|
+
</Card>
|
|
312
|
+
|
|
313
|
+
// ✅ Or use className on Card itself
|
|
314
|
+
<Card className={css({ p: 'lg' })}>
|
|
315
|
+
Content
|
|
316
|
+
</Card>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Testing
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { render, screen } from '@testing-library/react';
|
|
323
|
+
import userEvent from '@testing-library/user-event';
|
|
324
|
+
|
|
325
|
+
test('interactive card handles clicks', async () => {
|
|
326
|
+
const handleClick = vi.fn();
|
|
327
|
+
render(
|
|
328
|
+
<Card interactive onClick={handleClick}>
|
|
329
|
+
Card Content
|
|
330
|
+
</Card>
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const card = screen.getByText('Card Content').closest('div');
|
|
334
|
+
await userEvent.click(card);
|
|
335
|
+
|
|
336
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('non-interactive card does not trigger clicks', async () => {
|
|
340
|
+
const handleClick = vi.fn();
|
|
341
|
+
render(
|
|
342
|
+
<Card onClick={handleClick}>
|
|
343
|
+
Card Content
|
|
344
|
+
</Card>
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const card = screen.getByText('Card Content').closest('div');
|
|
348
|
+
await userEvent.click(card);
|
|
349
|
+
|
|
350
|
+
// interactive is false by default, so onClick should not be called
|
|
351
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
```
|