@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.
@@ -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
+ ```