@discourser/design-system 0.3.1 → 0.5.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 +12 -4
- package/dist/styles.css +5126 -0
- package/guidelines/Guidelines.md +92 -41
- package/guidelines/components/accordion.md +732 -0
- package/guidelines/components/avatar.md +1015 -0
- package/guidelines/components/badge.md +728 -0
- package/guidelines/components/button.md +75 -40
- package/guidelines/components/card.md +84 -25
- package/guidelines/components/checkbox.md +671 -0
- package/guidelines/components/dialog.md +619 -31
- package/guidelines/components/drawer.md +1616 -0
- package/guidelines/components/heading.md +576 -0
- package/guidelines/components/icon-button.md +92 -37
- package/guidelines/components/input-addon.md +685 -0
- package/guidelines/components/input-group.md +830 -0
- package/guidelines/components/input.md +92 -37
- package/guidelines/components/popover.md +1271 -0
- package/guidelines/components/progress.md +836 -0
- package/guidelines/components/radio-group.md +852 -0
- package/guidelines/components/select.md +1662 -0
- package/guidelines/components/skeleton.md +802 -0
- package/guidelines/components/slider.md +911 -0
- package/guidelines/components/spinner.md +783 -0
- package/guidelines/components/switch.md +105 -38
- package/guidelines/components/tabs.md +1488 -0
- package/guidelines/components/textarea.md +495 -0
- package/guidelines/components/toast.md +784 -0
- package/guidelines/components/tooltip.md +912 -0
- package/guidelines/design-tokens/colors.md +309 -72
- package/guidelines/design-tokens/elevation.md +615 -45
- package/guidelines/design-tokens/spacing.md +654 -74
- package/guidelines/design-tokens/typography.md +432 -50
- package/guidelines/overview-components.md +60 -8
- package/guidelines/overview-imports.md +314 -0
- package/guidelines/overview-patterns.md +3852 -0
- package/package.json +4 -2
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
**Purpose:** Visual representation of users or entities through images, initials, or icons, providing identity context throughout the application.
|
|
4
|
+
|
|
5
|
+
## When to Use This Component
|
|
6
|
+
|
|
7
|
+
Use Avatar when you need to **visually represent a user, person, or entity** with an image, initials, or icon to provide identity context.
|
|
8
|
+
|
|
9
|
+
### Decision Tree
|
|
10
|
+
|
|
11
|
+
| Scenario | Use Avatar? | Alternative | Reasoning |
|
|
12
|
+
| ------------------------- | ----------- | --------------------------- | ------------------------------------------------ |
|
|
13
|
+
| User profile pictures | ✅ Yes | - | Avatar is designed for user representation |
|
|
14
|
+
| Comment author indicators | ✅ Yes | - | Shows who created the content |
|
|
15
|
+
| Team member lists | ✅ Yes | - | Visual identification of people |
|
|
16
|
+
| Status labels or tags | ❌ No | Badge | Badge is for status, not identity |
|
|
17
|
+
| Decorative icons | ❌ No | Icon component | Use semantic icons for UI elements |
|
|
18
|
+
| Company logos (non-user) | ⚠️ Maybe | Image with `shape="square"` | Avatar with square shape works for organizations |
|
|
19
|
+
|
|
20
|
+
### Component Comparison
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ✅ Avatar - User profile representation
|
|
24
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
25
|
+
<Avatar.Fallback name="John Doe" />
|
|
26
|
+
<Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
|
|
27
|
+
</Avatar.Root>
|
|
28
|
+
|
|
29
|
+
// ❌ Don't use Avatar for status indicators - Use Badge
|
|
30
|
+
<Avatar.Root colorPalette="success" size="sm">
|
|
31
|
+
<Avatar.Fallback>✓</Avatar.Fallback>
|
|
32
|
+
</Avatar.Root>
|
|
33
|
+
|
|
34
|
+
// ✅ Better: Use Badge for status
|
|
35
|
+
<Badge colorPalette="success">Active</Badge>
|
|
36
|
+
|
|
37
|
+
// ❌ Don't use Avatar for UI icons - Use Icon
|
|
38
|
+
<Avatar.Root size="sm">
|
|
39
|
+
<Avatar.Fallback>
|
|
40
|
+
<SettingsIcon />
|
|
41
|
+
</Avatar.Fallback>
|
|
42
|
+
</Avatar.Root>
|
|
43
|
+
|
|
44
|
+
// ✅ Better: Use IconButton for UI actions
|
|
45
|
+
<IconButton aria-label="Settings">
|
|
46
|
+
<SettingsIcon />
|
|
47
|
+
</IconButton>
|
|
48
|
+
|
|
49
|
+
// ✅ Avatar - Comment author with status
|
|
50
|
+
<HStack gap="3">
|
|
51
|
+
<Box position="relative">
|
|
52
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
53
|
+
<Avatar.Fallback name="Jane Smith" />
|
|
54
|
+
<Avatar.Image src="/avatars/jane.jpg" alt="Jane Smith" />
|
|
55
|
+
</Avatar.Root>
|
|
56
|
+
{/* Online status indicator */}
|
|
57
|
+
<Box
|
|
58
|
+
position="absolute"
|
|
59
|
+
bottom="0"
|
|
60
|
+
right="0"
|
|
61
|
+
width="3"
|
|
62
|
+
height="3"
|
|
63
|
+
borderRadius="full"
|
|
64
|
+
bg="success.solid"
|
|
65
|
+
border="2px solid white"
|
|
66
|
+
/>
|
|
67
|
+
</Box>
|
|
68
|
+
<Stack gap="1">
|
|
69
|
+
<Text fontWeight="semibold">Jane Smith</Text>
|
|
70
|
+
<Text fontSize="sm" color="fg.muted">2 hours ago</Text>
|
|
71
|
+
</Stack>
|
|
72
|
+
</HStack>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Import
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import * as Avatar from '@discourser/design-system';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Component Structure
|
|
82
|
+
|
|
83
|
+
Avatar uses a compound component pattern with these parts:
|
|
84
|
+
|
|
85
|
+
- `Avatar.Root` - Container for the avatar
|
|
86
|
+
- `Avatar.Image` - Image element for avatar photo
|
|
87
|
+
- `Avatar.Fallback` - Fallback content when image fails or is loading
|
|
88
|
+
- `Avatar.Context` - Context hook for accessing avatar state
|
|
89
|
+
- `Avatar.RootProvider` - Root provider for advanced context usage
|
|
90
|
+
|
|
91
|
+
## Variants
|
|
92
|
+
|
|
93
|
+
The Avatar component supports 4 visual variants, each with specific use cases:
|
|
94
|
+
|
|
95
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
96
|
+
| --------- | -------------------------------------------- | ------------------------- | --------------------------------------------------- |
|
|
97
|
+
| `subtle` | Light background with colored text | Default avatars | General user representations, comments, posts |
|
|
98
|
+
| `solid` | Solid color background with contrasting text | High emphasis identifiers | Important users, featured profiles, emphasis needed |
|
|
99
|
+
| `surface` | Surface background with border | Outlined avatars | Cards, lists, secondary emphasis |
|
|
100
|
+
| `outline` | Transparent background with border | Minimal emphasis | Subtle user indicators, ghost profiles |
|
|
101
|
+
|
|
102
|
+
### Visual Characteristics
|
|
103
|
+
|
|
104
|
+
- **subtle**: Uses `colorPalette.subtle.bg` background with `colorPalette.subtle.fg` text (default)
|
|
105
|
+
- **solid**: Uses `colorPalette.solid.bg` background with `colorPalette.solid.fg` text (highest contrast)
|
|
106
|
+
- **surface**: Uses `colorPalette.surface.bg` background with 1px border and `colorPalette.surface.fg` text
|
|
107
|
+
- **outline**: Transparent background with 1px `colorPalette.outline.border` and `colorPalette.outline.fg` text
|
|
108
|
+
|
|
109
|
+
## Shapes
|
|
110
|
+
|
|
111
|
+
| Shape | Visual Style | Usage | When to Use |
|
|
112
|
+
| --------- | --------------------------- | ----------------- | ------------------------------------------------ |
|
|
113
|
+
| `full` | Fully circular avatar | User profiles | Default, most use cases, follows common patterns |
|
|
114
|
+
| `rounded` | Rounded corners (l3 radius) | Alternative style | Brand consistency, modern UI |
|
|
115
|
+
| `square` | Sharp corners | Non-user entities | Organizations, groups, system accounts |
|
|
116
|
+
|
|
117
|
+
**Recommendation:** Use `full` for individual users. Use `square` for organizations or system entities.
|
|
118
|
+
|
|
119
|
+
## Sizes
|
|
120
|
+
|
|
121
|
+
| Size | Dimensions | Font Size | Icon Size | Usage |
|
|
122
|
+
| ------ | ---------- | --------- | ---------- | ----------------------------------------------- |
|
|
123
|
+
| `2xs` | 24px (6) | 2xs | 12px (3) | Tiny indicators, inline mentions, compact lists |
|
|
124
|
+
| `xs` | 32px (8) | xs | 16px (4) | Dense layouts, small avatars in groups |
|
|
125
|
+
| `sm` | 36px (9) | sm | 18px (4.5) | Comments, compact user lists, secondary areas |
|
|
126
|
+
| `md` | 40px (10) | md | 20px (5) | Default, most use cases, navigation bars |
|
|
127
|
+
| `lg` | 44px (11) | md | 22px (5.5) | User cards, prominent displays |
|
|
128
|
+
| `xl` | 48px (12) | lg | 24px (6) | Profile headers, featured users |
|
|
129
|
+
| `2xl` | 64px (16) | xl | 32px (8) | Large profile displays, hero sections |
|
|
130
|
+
| `full` | 100% | 100% | Responsive | Container-sized avatars, flexible layouts |
|
|
131
|
+
|
|
132
|
+
**Recommendation:** Use `md` for most cases. Use `sm` or `xs` for compact lists. Use `lg` or larger for profile pages and emphasis.
|
|
133
|
+
|
|
134
|
+
## Props
|
|
135
|
+
|
|
136
|
+
### Root Props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
| -------------- | ------------------------------------------------------------------ | ---------- | --------------------------------------------------------- |
|
|
140
|
+
| `size` | `'2xs' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| 'full'` | `'md'` | Avatar size |
|
|
141
|
+
| `variant` | `'subtle' \| 'solid' \| 'surface' \| 'outline'` | `'subtle'` | Visual style variant |
|
|
142
|
+
| `shape` | `'full' \| 'rounded' \| 'square'` | `'full'` | Avatar shape |
|
|
143
|
+
| `colorPalette` | `string` | - | Color palette for the avatar (e.g., 'primary', 'neutral') |
|
|
144
|
+
| `className` | `string` | - | Additional CSS classes (use sparingly) |
|
|
145
|
+
|
|
146
|
+
### Image Props
|
|
147
|
+
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
| ---------------- | --------- | --------------- | ------------------------------------------- |
|
|
150
|
+
| `src` | `string` | Required | Image source URL |
|
|
151
|
+
| `alt` | `string` | Required | Alt text for accessibility |
|
|
152
|
+
| `draggable` | `boolean` | `false` | Whether image is draggable (default: false) |
|
|
153
|
+
| `referrerPolicy` | `string` | `'no-referrer'` | Referrer policy for image loading |
|
|
154
|
+
|
|
155
|
+
### Fallback Props
|
|
156
|
+
|
|
157
|
+
| Prop | Type | Default | Description |
|
|
158
|
+
| ---------- | ----------- | ------- | ------------------------------------------------------- |
|
|
159
|
+
| `name` | `string` | - | Name to derive initials from (e.g., "John Doe" → "JD") |
|
|
160
|
+
| `children` | `ReactNode` | - | Custom fallback content (overrides name-based initials) |
|
|
161
|
+
|
|
162
|
+
**Note:** Avatar components extend their respective HTML element attributes, so all standard attributes are supported.
|
|
163
|
+
|
|
164
|
+
## Automatic Initials Generation
|
|
165
|
+
|
|
166
|
+
The `Avatar.Fallback` component automatically generates initials from the `name` prop:
|
|
167
|
+
|
|
168
|
+
- **Full name**: "John Doe" → "JD" (first letter of first and last name)
|
|
169
|
+
- **Single name**: "John" → "J" (first letter only)
|
|
170
|
+
- **Multiple names**: "Mary Jane Watson" → "MW" (first and last name only)
|
|
171
|
+
- **No name**: Displays default user icon
|
|
172
|
+
|
|
173
|
+
## Examples
|
|
174
|
+
|
|
175
|
+
### Basic Usage
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import * as Avatar from '@discourser/design-system';
|
|
179
|
+
|
|
180
|
+
// Avatar with image
|
|
181
|
+
<Avatar.Root colorPalette="primary">
|
|
182
|
+
<Avatar.Fallback name="John Doe" />
|
|
183
|
+
<Avatar.Image src="https://example.com/avatar.jpg" alt="John Doe" />
|
|
184
|
+
</Avatar.Root>
|
|
185
|
+
|
|
186
|
+
// Avatar with initials only
|
|
187
|
+
<Avatar.Root colorPalette="primary">
|
|
188
|
+
<Avatar.Fallback name="Sarah Williams" />
|
|
189
|
+
</Avatar.Root>
|
|
190
|
+
|
|
191
|
+
// Avatar with default icon (no name)
|
|
192
|
+
<Avatar.Root colorPalette="primary">
|
|
193
|
+
<Avatar.Fallback />
|
|
194
|
+
</Avatar.Root>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Image Avatars with Fallback
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Image with automatic fallback
|
|
201
|
+
// If image fails to load, shows initials
|
|
202
|
+
<Avatar.Root colorPalette="primary">
|
|
203
|
+
<Avatar.Fallback name="Michael Chen" />
|
|
204
|
+
<Avatar.Image
|
|
205
|
+
src="https://api.example.com/users/123/avatar"
|
|
206
|
+
alt="Michael Chen"
|
|
207
|
+
/>
|
|
208
|
+
</Avatar.Root>
|
|
209
|
+
|
|
210
|
+
// Multiple avatars with consistent fallback
|
|
211
|
+
const users = [
|
|
212
|
+
{ name: "Alice Johnson", avatar: "https://i.pravatar.cc/150?img=1" },
|
|
213
|
+
{ name: "Bob Smith", avatar: "https://i.pravatar.cc/150?img=2" },
|
|
214
|
+
{ name: "Carol White", avatar: "https://i.pravatar.cc/150?img=3" },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
{users.map(user => (
|
|
218
|
+
<Avatar.Root key={user.name} colorPalette="primary">
|
|
219
|
+
<Avatar.Fallback name={user.name} />
|
|
220
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
221
|
+
</Avatar.Root>
|
|
222
|
+
))}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Initials-Only Avatars
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Default variant with initials
|
|
229
|
+
<Avatar.Root colorPalette="primary">
|
|
230
|
+
<Avatar.Fallback name="Emily Rodriguez" />
|
|
231
|
+
</Avatar.Root>
|
|
232
|
+
|
|
233
|
+
// Different variants
|
|
234
|
+
<Avatar.Root colorPalette="primary" variant="subtle">
|
|
235
|
+
<Avatar.Fallback name="David Kim" />
|
|
236
|
+
</Avatar.Root>
|
|
237
|
+
|
|
238
|
+
<Avatar.Root colorPalette="primary" variant="solid">
|
|
239
|
+
<Avatar.Fallback name="Lisa Anderson" />
|
|
240
|
+
</Avatar.Root>
|
|
241
|
+
|
|
242
|
+
<Avatar.Root colorPalette="primary" variant="surface">
|
|
243
|
+
<Avatar.Fallback name="Tom Baker" />
|
|
244
|
+
</Avatar.Root>
|
|
245
|
+
|
|
246
|
+
<Avatar.Root colorPalette="primary" variant="outline">
|
|
247
|
+
<Avatar.Fallback name="Nina Patel" />
|
|
248
|
+
</Avatar.Root>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Custom Fallback Content
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Custom icon fallback
|
|
255
|
+
<Avatar.Root colorPalette="primary">
|
|
256
|
+
<Avatar.Fallback>
|
|
257
|
+
<CustomUserIcon />
|
|
258
|
+
</Avatar.Fallback>
|
|
259
|
+
</Avatar.Root>
|
|
260
|
+
|
|
261
|
+
// Custom text fallback
|
|
262
|
+
<Avatar.Root colorPalette="primary">
|
|
263
|
+
<Avatar.Fallback>?</Avatar.Fallback>
|
|
264
|
+
</Avatar.Root>
|
|
265
|
+
|
|
266
|
+
// Badge-style fallback
|
|
267
|
+
<Avatar.Root colorPalette="success">
|
|
268
|
+
<Avatar.Fallback>✓</Avatar.Fallback>
|
|
269
|
+
</Avatar.Root>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Different Sizes
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Extra small - for inline mentions
|
|
276
|
+
<Avatar.Root colorPalette="primary" size="2xs">
|
|
277
|
+
<Avatar.Fallback name="John Doe" />
|
|
278
|
+
</Avatar.Root>
|
|
279
|
+
|
|
280
|
+
// Small - for comments and compact lists
|
|
281
|
+
<Avatar.Root colorPalette="primary" size="sm">
|
|
282
|
+
<Avatar.Fallback name="Jane Smith" />
|
|
283
|
+
<Avatar.Image src="/avatars/jane.jpg" alt="Jane Smith" />
|
|
284
|
+
</Avatar.Root>
|
|
285
|
+
|
|
286
|
+
// Default - general use
|
|
287
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
288
|
+
<Avatar.Fallback name="Mike Johnson" />
|
|
289
|
+
<Avatar.Image src="/avatars/mike.jpg" alt="Mike Johnson" />
|
|
290
|
+
</Avatar.Root>
|
|
291
|
+
|
|
292
|
+
// Large - profile displays
|
|
293
|
+
<Avatar.Root colorPalette="primary" size="lg">
|
|
294
|
+
<Avatar.Fallback name="Sarah Williams" />
|
|
295
|
+
<Avatar.Image src="/avatars/sarah.jpg" alt="Sarah Williams" />
|
|
296
|
+
</Avatar.Root>
|
|
297
|
+
|
|
298
|
+
// Extra large - profile headers
|
|
299
|
+
<Avatar.Root colorPalette="primary" size="2xl">
|
|
300
|
+
<Avatar.Fallback name="David Chen" />
|
|
301
|
+
<Avatar.Image src="/avatars/david.jpg" alt="David Chen" />
|
|
302
|
+
</Avatar.Root>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Different Shapes
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Circular (default) - for users
|
|
309
|
+
<Avatar.Root colorPalette="primary" shape="full">
|
|
310
|
+
<Avatar.Fallback name="John Doe" />
|
|
311
|
+
</Avatar.Root>
|
|
312
|
+
|
|
313
|
+
// Rounded - modern style
|
|
314
|
+
<Avatar.Root colorPalette="primary" shape="rounded">
|
|
315
|
+
<Avatar.Fallback name="Jane Smith" />
|
|
316
|
+
</Avatar.Root>
|
|
317
|
+
|
|
318
|
+
// Square - for organizations
|
|
319
|
+
<Avatar.Root colorPalette="neutral" shape="square">
|
|
320
|
+
<Avatar.Fallback name="Acme Corp" />
|
|
321
|
+
<Avatar.Image src="/logos/acme.png" alt="Acme Corp" />
|
|
322
|
+
</Avatar.Root>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Color Palettes
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// Different color palettes for variety
|
|
329
|
+
<Avatar.Root colorPalette="primary">
|
|
330
|
+
<Avatar.Fallback name="User One" />
|
|
331
|
+
</Avatar.Root>
|
|
332
|
+
|
|
333
|
+
<Avatar.Root colorPalette="neutral">
|
|
334
|
+
<Avatar.Fallback name="User Two" />
|
|
335
|
+
</Avatar.Root>
|
|
336
|
+
|
|
337
|
+
<Avatar.Root colorPalette="error">
|
|
338
|
+
<Avatar.Fallback name="User Three" />
|
|
339
|
+
</Avatar.Root>
|
|
340
|
+
|
|
341
|
+
<Avatar.Root colorPalette="success">
|
|
342
|
+
<Avatar.Fallback name="User Four" />
|
|
343
|
+
</Avatar.Root>
|
|
344
|
+
|
|
345
|
+
// Use color palette to represent different user types
|
|
346
|
+
const getColorForRole = (role: string) => {
|
|
347
|
+
const colors = {
|
|
348
|
+
admin: 'error',
|
|
349
|
+
moderator: 'warning',
|
|
350
|
+
member: 'primary',
|
|
351
|
+
guest: 'neutral',
|
|
352
|
+
};
|
|
353
|
+
return colors[role] || 'primary';
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
<Avatar.Root colorPalette={getColorForRole(user.role)}>
|
|
357
|
+
<Avatar.Fallback name={user.name} />
|
|
358
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
359
|
+
</Avatar.Root>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Common Patterns
|
|
363
|
+
|
|
364
|
+
### Avatar with Status Indicator
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { css } from 'styled-system/css';
|
|
368
|
+
|
|
369
|
+
// Avatar with online status
|
|
370
|
+
<div className={css({ position: 'relative', display: 'inline-flex' })}>
|
|
371
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
372
|
+
<Avatar.Fallback name="John Doe" />
|
|
373
|
+
<Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
|
|
374
|
+
</Avatar.Root>
|
|
375
|
+
|
|
376
|
+
{/* Status indicator */}
|
|
377
|
+
<div className={css({
|
|
378
|
+
position: 'absolute',
|
|
379
|
+
bottom: '0',
|
|
380
|
+
right: '0',
|
|
381
|
+
width: '3',
|
|
382
|
+
height: '3',
|
|
383
|
+
borderRadius: 'full',
|
|
384
|
+
bg: 'success.solid',
|
|
385
|
+
border: '2px solid',
|
|
386
|
+
borderColor: 'bg.canvas',
|
|
387
|
+
})} />
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
// Avatar with availability status
|
|
391
|
+
const StatusAvatar = ({ name, src, status }: Props) => {
|
|
392
|
+
const statusColors = {
|
|
393
|
+
online: 'success.solid',
|
|
394
|
+
away: 'warning.solid',
|
|
395
|
+
busy: 'error.solid',
|
|
396
|
+
offline: 'neutral.subtle',
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div className={css({ position: 'relative', display: 'inline-flex' })}>
|
|
401
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
402
|
+
<Avatar.Fallback name={name} />
|
|
403
|
+
<Avatar.Image src={src} alt={name} />
|
|
404
|
+
</Avatar.Root>
|
|
405
|
+
|
|
406
|
+
<div className={css({
|
|
407
|
+
position: 'absolute',
|
|
408
|
+
bottom: '0',
|
|
409
|
+
right: '0',
|
|
410
|
+
width: '3',
|
|
411
|
+
height: '3',
|
|
412
|
+
borderRadius: 'full',
|
|
413
|
+
bg: statusColors[status],
|
|
414
|
+
border: '2px solid',
|
|
415
|
+
borderColor: 'bg.canvas',
|
|
416
|
+
})} />
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Avatar Group (Stacked)
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { HStack } from 'styled-system/jsx';
|
|
426
|
+
|
|
427
|
+
// Overlapping avatar group
|
|
428
|
+
<HStack gap="-3">
|
|
429
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
430
|
+
<Avatar.Fallback name="Alice Johnson" />
|
|
431
|
+
<Avatar.Image src="/avatars/alice.jpg" alt="Alice" />
|
|
432
|
+
</Avatar.Root>
|
|
433
|
+
|
|
434
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
435
|
+
<Avatar.Fallback name="Bob Smith" />
|
|
436
|
+
<Avatar.Image src="/avatars/bob.jpg" alt="Bob" />
|
|
437
|
+
</Avatar.Root>
|
|
438
|
+
|
|
439
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
440
|
+
<Avatar.Fallback name="Carol White" />
|
|
441
|
+
<Avatar.Image src="/avatars/carol.jpg" alt="Carol" />
|
|
442
|
+
</Avatar.Root>
|
|
443
|
+
|
|
444
|
+
{/* Count indicator for additional users */}
|
|
445
|
+
<Avatar.Root colorPalette="neutral" size="md">
|
|
446
|
+
<Avatar.Fallback>+5</Avatar.Fallback>
|
|
447
|
+
</Avatar.Root>
|
|
448
|
+
</HStack>
|
|
449
|
+
|
|
450
|
+
// Reusable avatar group component
|
|
451
|
+
const AvatarGroup = ({ users, max = 3, size = 'md' }: Props) => {
|
|
452
|
+
const visibleUsers = users.slice(0, max);
|
|
453
|
+
const remainingCount = users.length - max;
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<HStack gap={size === 'sm' ? '-2' : '-3'}>
|
|
457
|
+
{visibleUsers.map(user => (
|
|
458
|
+
<Avatar.Root key={user.id} colorPalette="primary" size={size}>
|
|
459
|
+
<Avatar.Fallback name={user.name} />
|
|
460
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
461
|
+
</Avatar.Root>
|
|
462
|
+
))}
|
|
463
|
+
|
|
464
|
+
{remainingCount > 0 && (
|
|
465
|
+
<Avatar.Root colorPalette="neutral" size={size}>
|
|
466
|
+
<Avatar.Fallback>+{remainingCount}</Avatar.Fallback>
|
|
467
|
+
</Avatar.Root>
|
|
468
|
+
)}
|
|
469
|
+
</HStack>
|
|
470
|
+
);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
<AvatarGroup users={teamMembers} max={4} size="md" />
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Avatar with Tooltip
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import * as Tooltip from '@discourser/design-system';
|
|
480
|
+
|
|
481
|
+
// Avatar with hover tooltip
|
|
482
|
+
<Tooltip.Root>
|
|
483
|
+
<Tooltip.Trigger asChild>
|
|
484
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
485
|
+
<Avatar.Fallback name="John Doe" />
|
|
486
|
+
<Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
|
|
487
|
+
</Avatar.Root>
|
|
488
|
+
</Tooltip.Trigger>
|
|
489
|
+
|
|
490
|
+
<Tooltip.Positioner>
|
|
491
|
+
<Tooltip.Content>
|
|
492
|
+
<Tooltip.Arrow />
|
|
493
|
+
John Doe - Senior Developer
|
|
494
|
+
</Tooltip.Content>
|
|
495
|
+
</Tooltip.Positioner>
|
|
496
|
+
</Tooltip.Root>
|
|
497
|
+
|
|
498
|
+
// Avatar group with individual tooltips
|
|
499
|
+
{users.map(user => (
|
|
500
|
+
<Tooltip.Root key={user.id}>
|
|
501
|
+
<Tooltip.Trigger asChild>
|
|
502
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
503
|
+
<Avatar.Fallback name={user.name} />
|
|
504
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
505
|
+
</Avatar.Root>
|
|
506
|
+
</Tooltip.Trigger>
|
|
507
|
+
|
|
508
|
+
<Tooltip.Positioner>
|
|
509
|
+
<Tooltip.Content>
|
|
510
|
+
<Tooltip.Arrow />
|
|
511
|
+
{user.name} - {user.role}
|
|
512
|
+
</Tooltip.Content>
|
|
513
|
+
</Tooltip.Positioner>
|
|
514
|
+
</Tooltip.Root>
|
|
515
|
+
))}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Avatar in User Card
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { VStack, HStack } from 'styled-system/jsx';
|
|
522
|
+
import { css } from 'styled-system/css';
|
|
523
|
+
|
|
524
|
+
const UserCard = ({ user }: Props) => (
|
|
525
|
+
<div className={css({
|
|
526
|
+
p: '4',
|
|
527
|
+
borderRadius: 'l2',
|
|
528
|
+
bg: 'bg.surface',
|
|
529
|
+
borderWidth: '1px',
|
|
530
|
+
borderColor: 'border.default',
|
|
531
|
+
})}>
|
|
532
|
+
<HStack gap="3" align="center">
|
|
533
|
+
<Avatar.Root colorPalette="primary" size="lg">
|
|
534
|
+
<Avatar.Fallback name={user.name} />
|
|
535
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
536
|
+
</Avatar.Root>
|
|
537
|
+
|
|
538
|
+
<VStack gap="1" align="start">
|
|
539
|
+
<div className={css({ fontWeight: 'semibold' })}>
|
|
540
|
+
{user.name}
|
|
541
|
+
</div>
|
|
542
|
+
<div className={css({ fontSize: 'sm', color: 'fg.subtle' })}>
|
|
543
|
+
{user.email}
|
|
544
|
+
</div>
|
|
545
|
+
</VStack>
|
|
546
|
+
</HStack>
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Avatar with Upload/Edit
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { css } from 'styled-system/css';
|
|
555
|
+
|
|
556
|
+
// Avatar with edit overlay
|
|
557
|
+
const EditableAvatar = ({ name, src, onEdit }: Props) => (
|
|
558
|
+
<div className={css({ position: 'relative', display: 'inline-flex' })}>
|
|
559
|
+
<Avatar.Root colorPalette="primary" size="2xl">
|
|
560
|
+
<Avatar.Fallback name={name} />
|
|
561
|
+
<Avatar.Image src={src} alt={name} />
|
|
562
|
+
</Avatar.Root>
|
|
563
|
+
|
|
564
|
+
{/* Edit overlay */}
|
|
565
|
+
<button
|
|
566
|
+
onClick={onEdit}
|
|
567
|
+
className={css({
|
|
568
|
+
position: 'absolute',
|
|
569
|
+
inset: '0',
|
|
570
|
+
borderRadius: 'full',
|
|
571
|
+
bg: 'blackAlpha.600',
|
|
572
|
+
color: 'white',
|
|
573
|
+
display: 'flex',
|
|
574
|
+
alignItems: 'center',
|
|
575
|
+
justifyContent: 'center',
|
|
576
|
+
opacity: '0',
|
|
577
|
+
transition: 'opacity 0.2s',
|
|
578
|
+
cursor: 'pointer',
|
|
579
|
+
_hover: { opacity: '1' },
|
|
580
|
+
})}
|
|
581
|
+
>
|
|
582
|
+
<CameraIcon />
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
);
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Loading State
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { Skeleton } from '@discourser/design-system';
|
|
592
|
+
|
|
593
|
+
// Avatar loading skeleton
|
|
594
|
+
<Skeleton.Root>
|
|
595
|
+
<Skeleton.Circle size="10" />
|
|
596
|
+
</Skeleton.Root>
|
|
597
|
+
|
|
598
|
+
// Conditional loading
|
|
599
|
+
const UserAvatar = ({ user, isLoading }: Props) => {
|
|
600
|
+
if (isLoading) {
|
|
601
|
+
return <Skeleton.Circle size="10" />;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
<Avatar.Root colorPalette="primary" size="md">
|
|
606
|
+
<Avatar.Fallback name={user.name} />
|
|
607
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
608
|
+
</Avatar.Root>
|
|
609
|
+
);
|
|
610
|
+
};
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Comment/Post Avatar
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
import { HStack, VStack } from 'styled-system/jsx';
|
|
617
|
+
import { css } from 'styled-system/css';
|
|
618
|
+
|
|
619
|
+
const Comment = ({ comment }: Props) => (
|
|
620
|
+
<HStack gap="3" align="start">
|
|
621
|
+
<Avatar.Root colorPalette="primary" size="sm">
|
|
622
|
+
<Avatar.Fallback name={comment.author.name} />
|
|
623
|
+
<Avatar.Image src={comment.author.avatar} alt={comment.author.name} />
|
|
624
|
+
</Avatar.Root>
|
|
625
|
+
|
|
626
|
+
<VStack gap="1" align="start" flex="1">
|
|
627
|
+
<HStack gap="2" align="center">
|
|
628
|
+
<span className={css({ fontWeight: 'semibold', fontSize: 'sm' })}>
|
|
629
|
+
{comment.author.name}
|
|
630
|
+
</span>
|
|
631
|
+
<span className={css({ fontSize: 'xs', color: 'fg.subtle' })}>
|
|
632
|
+
{comment.timestamp}
|
|
633
|
+
</span>
|
|
634
|
+
</HStack>
|
|
635
|
+
|
|
636
|
+
<p className={css({ fontSize: 'sm' })}>
|
|
637
|
+
{comment.content}
|
|
638
|
+
</p>
|
|
639
|
+
</VStack>
|
|
640
|
+
</HStack>
|
|
641
|
+
);
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Navigation User Menu
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
import { HStack } from 'styled-system/jsx';
|
|
648
|
+
import { css } from 'styled-system/css';
|
|
649
|
+
|
|
650
|
+
const UserMenu = ({ user }: Props) => (
|
|
651
|
+
<HStack gap="2" align="center">
|
|
652
|
+
<Avatar.Root colorPalette="primary" size="sm">
|
|
653
|
+
<Avatar.Fallback name={user.name} />
|
|
654
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
655
|
+
</Avatar.Root>
|
|
656
|
+
|
|
657
|
+
<span className={css({ fontSize: 'sm', fontWeight: 'medium' })}>
|
|
658
|
+
{user.name}
|
|
659
|
+
</span>
|
|
660
|
+
|
|
661
|
+
<ChevronDownIcon />
|
|
662
|
+
</HStack>
|
|
663
|
+
);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
## DO NOT
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// ❌ Don't forget the fallback (image might fail to load)
|
|
670
|
+
<Avatar.Root colorPalette="primary">
|
|
671
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
672
|
+
</Avatar.Root>
|
|
673
|
+
|
|
674
|
+
// ✅ Always provide a fallback
|
|
675
|
+
<Avatar.Root colorPalette="primary">
|
|
676
|
+
<Avatar.Fallback name={user.name} />
|
|
677
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
678
|
+
</Avatar.Root>
|
|
679
|
+
|
|
680
|
+
// ❌ Don't forget alt text for accessibility
|
|
681
|
+
<Avatar.Image src={user.avatar} />
|
|
682
|
+
|
|
683
|
+
// ✅ Always provide meaningful alt text
|
|
684
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
685
|
+
|
|
686
|
+
// ❌ Don't use inline styles
|
|
687
|
+
<Avatar.Root style={{ width: '60px' }}>
|
|
688
|
+
<Avatar.Fallback name="User" />
|
|
689
|
+
</Avatar.Root>
|
|
690
|
+
|
|
691
|
+
// ✅ Use size prop
|
|
692
|
+
<Avatar.Root size="xl">
|
|
693
|
+
<Avatar.Fallback name="User" />
|
|
694
|
+
</Avatar.Root>
|
|
695
|
+
|
|
696
|
+
// ❌ Don't use Image without Root
|
|
697
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
698
|
+
|
|
699
|
+
// ✅ Always wrap with Root
|
|
700
|
+
<Avatar.Root colorPalette="primary">
|
|
701
|
+
<Avatar.Fallback name={user.name} />
|
|
702
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
703
|
+
</Avatar.Root>
|
|
704
|
+
|
|
705
|
+
// ❌ Don't hardcode initials
|
|
706
|
+
<Avatar.Root colorPalette="primary">
|
|
707
|
+
<Avatar.Fallback>JD</Avatar.Fallback>
|
|
708
|
+
</Avatar.Root>
|
|
709
|
+
|
|
710
|
+
// ✅ Let the component generate initials
|
|
711
|
+
<Avatar.Root colorPalette="primary">
|
|
712
|
+
<Avatar.Fallback name="John Doe" />
|
|
713
|
+
</Avatar.Root>
|
|
714
|
+
|
|
715
|
+
// ❌ Don't use different sizes in avatar groups
|
|
716
|
+
<HStack gap="-3">
|
|
717
|
+
<Avatar.Root size="md"><Avatar.Fallback name="User 1" /></Avatar.Root>
|
|
718
|
+
<Avatar.Root size="lg"><Avatar.Fallback name="User 2" /></Avatar.Root>
|
|
719
|
+
</HStack>
|
|
720
|
+
|
|
721
|
+
// ✅ Keep consistent sizes in groups
|
|
722
|
+
<HStack gap="-3">
|
|
723
|
+
<Avatar.Root size="md"><Avatar.Fallback name="User 1" /></Avatar.Root>
|
|
724
|
+
<Avatar.Root size="md"><Avatar.Fallback name="User 2" /></Avatar.Root>
|
|
725
|
+
</HStack>
|
|
726
|
+
|
|
727
|
+
// ❌ Don't mix shapes in the same context
|
|
728
|
+
<HStack gap="2">
|
|
729
|
+
<Avatar.Root shape="full"><Avatar.Fallback name="User 1" /></Avatar.Root>
|
|
730
|
+
<Avatar.Root shape="square"><Avatar.Fallback name="User 2" /></Avatar.Root>
|
|
731
|
+
</HStack>
|
|
732
|
+
|
|
733
|
+
// ✅ Use consistent shapes in the same context
|
|
734
|
+
<HStack gap="2">
|
|
735
|
+
<Avatar.Root shape="full"><Avatar.Fallback name="User 1" /></Avatar.Root>
|
|
736
|
+
<Avatar.Root shape="full"><Avatar.Fallback name="User 2" /></Avatar.Root>
|
|
737
|
+
</HStack>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
## Accessibility
|
|
741
|
+
|
|
742
|
+
The Avatar component follows WCAG 2.1 Level AA standards:
|
|
743
|
+
|
|
744
|
+
- **Alt Text**: Always provide descriptive alt text for Avatar.Image
|
|
745
|
+
- **Semantic HTML**: Uses proper img elements with alt attributes
|
|
746
|
+
- **Color Contrast**: All variants meet 4.5:1 contrast ratio for text/initials
|
|
747
|
+
- **Non-decorative**: Images convey user identity and should have meaningful alt text
|
|
748
|
+
- **Keyboard Navigation**: If interactive (e.g., clickable), ensure proper keyboard support
|
|
749
|
+
|
|
750
|
+
### Accessibility Best Practices
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
// ✅ Provide meaningful alt text
|
|
754
|
+
<Avatar.Root colorPalette="primary">
|
|
755
|
+
<Avatar.Fallback name="Sarah Johnson" />
|
|
756
|
+
<Avatar.Image
|
|
757
|
+
src="/avatars/sarah.jpg"
|
|
758
|
+
alt="Sarah Johnson's profile picture"
|
|
759
|
+
/>
|
|
760
|
+
</Avatar.Root>
|
|
761
|
+
|
|
762
|
+
// ✅ For clickable avatars, add proper labels
|
|
763
|
+
<button aria-label="View John Doe's profile">
|
|
764
|
+
<Avatar.Root colorPalette="primary">
|
|
765
|
+
<Avatar.Fallback name="John Doe" />
|
|
766
|
+
<Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
|
|
767
|
+
</Avatar.Root>
|
|
768
|
+
</button>
|
|
769
|
+
|
|
770
|
+
// ✅ For decorative avatars in groups (when text is nearby)
|
|
771
|
+
<HStack gap="2" align="center">
|
|
772
|
+
<Avatar.Root colorPalette="primary" size="sm">
|
|
773
|
+
<Avatar.Fallback name="Jane Smith" />
|
|
774
|
+
<Avatar.Image src="/avatars/jane.jpg" alt="" role="presentation" />
|
|
775
|
+
</Avatar.Root>
|
|
776
|
+
<span>Jane Smith</span>
|
|
777
|
+
</HStack>
|
|
778
|
+
|
|
779
|
+
// ✅ Provide context for avatar groups
|
|
780
|
+
<div role="group" aria-label="Team members">
|
|
781
|
+
<HStack gap="-3">
|
|
782
|
+
{members.map(member => (
|
|
783
|
+
<Avatar.Root key={member.id} colorPalette="primary">
|
|
784
|
+
<Avatar.Fallback name={member.name} />
|
|
785
|
+
<Avatar.Image src={member.avatar} alt={member.name} />
|
|
786
|
+
</Avatar.Root>
|
|
787
|
+
))}
|
|
788
|
+
</HStack>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
// ✅ Status indicators should be announced
|
|
792
|
+
<div role="img" aria-label="Sarah Johnson, online">
|
|
793
|
+
<Avatar.Root colorPalette="primary">
|
|
794
|
+
<Avatar.Fallback name="Sarah Johnson" />
|
|
795
|
+
<Avatar.Image src="/avatars/sarah.jpg" alt="" />
|
|
796
|
+
</Avatar.Root>
|
|
797
|
+
<span className={css({ srOnly: true })}>Online</span>
|
|
798
|
+
<div className={css({ /* status indicator styles */ })} />
|
|
799
|
+
</div>
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
## Size Selection Guide
|
|
803
|
+
|
|
804
|
+
| Scenario | Recommended Size | Reasoning |
|
|
805
|
+
| --------------- | ---------------- | --------------------------------------- |
|
|
806
|
+
| Inline mentions | `2xs` | Minimal disruption to text flow |
|
|
807
|
+
| Comment threads | `sm` | Compact, doesn't overwhelm content |
|
|
808
|
+
| User lists | `sm` or `md` | Balance between recognition and density |
|
|
809
|
+
| Navigation bar | `sm` or `md` | Subtle presence, space-efficient |
|
|
810
|
+
| User cards | `lg` | Prominent, easy to recognize |
|
|
811
|
+
| Profile headers | `xl` or `2xl` | Hero display, maximum recognition |
|
|
812
|
+
| Avatar groups | `sm` or `md` | Better overlap appearance |
|
|
813
|
+
| Sidebar users | `md` | Standard size for side navigation |
|
|
814
|
+
| Chat messages | `sm` | Space-efficient for message threads |
|
|
815
|
+
| Settings pages | `lg` | Clear identity context |
|
|
816
|
+
|
|
817
|
+
## Variant Selection Guide
|
|
818
|
+
|
|
819
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
820
|
+
| ----------------- | -------------------- | ------------------------------------------------ |
|
|
821
|
+
| Default users | `subtle` | Soft, unobtrusive, works everywhere |
|
|
822
|
+
| Featured users | `solid` | High contrast, draws attention |
|
|
823
|
+
| User cards | `surface` | Defined boundaries, works on colored backgrounds |
|
|
824
|
+
| Minimal UI | `outline` | Lightweight, modern appearance |
|
|
825
|
+
| Dark backgrounds | `solid` or `surface` | Better contrast and visibility |
|
|
826
|
+
| Light backgrounds | `subtle` | Subtle, doesn't compete with content |
|
|
827
|
+
|
|
828
|
+
## Shape Selection Guide
|
|
829
|
+
|
|
830
|
+
| Scenario | Recommended Shape | Reasoning |
|
|
831
|
+
| ---------------- | --------------------- | ---------------------------------------- |
|
|
832
|
+
| Individual users | `full` | Standard convention, friendly appearance |
|
|
833
|
+
| Organizations | `square` | Distinct from users, formal |
|
|
834
|
+
| Brand logos | `square` or `rounded` | Preserves logo shape and design |
|
|
835
|
+
| Bot accounts | `rounded` | Different from users, still approachable |
|
|
836
|
+
| System users | `square` | Indicates non-human entity |
|
|
837
|
+
|
|
838
|
+
## State Behaviors
|
|
839
|
+
|
|
840
|
+
| State | Visual Change | Behavior |
|
|
841
|
+
| -------------------------- | ------------------------ | ---------------------------------------------- |
|
|
842
|
+
| **Loading** | Shows fallback | Fallback displays while image loads |
|
|
843
|
+
| **Error** | Shows fallback | Fallback displays if image fails to load |
|
|
844
|
+
| **No Image** | Shows fallback | Displays initials or default icon |
|
|
845
|
+
| **Hover** (if interactive) | Optional visual feedback | Add cursor: pointer and hover styles as needed |
|
|
846
|
+
|
|
847
|
+
## Responsive Considerations
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// Responsive avatar sizes
|
|
851
|
+
import { css } from 'styled-system/css';
|
|
852
|
+
|
|
853
|
+
<Avatar.Root
|
|
854
|
+
colorPalette="primary"
|
|
855
|
+
className={css({
|
|
856
|
+
// Use size tokens for responsive sizing
|
|
857
|
+
size: { base: '8', md: '10', lg: '12' }
|
|
858
|
+
})}
|
|
859
|
+
>
|
|
860
|
+
<Avatar.Fallback name="John Doe" />
|
|
861
|
+
<Avatar.Image src="/avatars/john.jpg" alt="John Doe" />
|
|
862
|
+
</Avatar.Root>
|
|
863
|
+
|
|
864
|
+
// Responsive avatar group
|
|
865
|
+
<HStack
|
|
866
|
+
gap={{ base: '-2', md: '-3' }}
|
|
867
|
+
className={css({
|
|
868
|
+
'& > *': {
|
|
869
|
+
size: { base: 'sm', md: 'md' }
|
|
870
|
+
}
|
|
871
|
+
})}
|
|
872
|
+
>
|
|
873
|
+
{users.map(user => (
|
|
874
|
+
<Avatar.Root key={user.id} colorPalette="primary">
|
|
875
|
+
<Avatar.Fallback name={user.name} />
|
|
876
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
877
|
+
</Avatar.Root>
|
|
878
|
+
))}
|
|
879
|
+
</HStack>
|
|
880
|
+
|
|
881
|
+
// Responsive profile header
|
|
882
|
+
<VStack gap={{ base: '3', md: '4' }} align="center">
|
|
883
|
+
<Avatar.Root
|
|
884
|
+
colorPalette="primary"
|
|
885
|
+
size={{ base: 'xl', md: '2xl' }}
|
|
886
|
+
>
|
|
887
|
+
<Avatar.Fallback name={user.name} />
|
|
888
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
889
|
+
</Avatar.Root>
|
|
890
|
+
|
|
891
|
+
<h1 className={css({
|
|
892
|
+
fontSize: { base: 'xl', md: '2xl' },
|
|
893
|
+
fontWeight: 'bold'
|
|
894
|
+
})}>
|
|
895
|
+
{user.name}
|
|
896
|
+
</h1>
|
|
897
|
+
</VStack>
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
## Testing
|
|
901
|
+
|
|
902
|
+
When testing Avatar components:
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
906
|
+
import * as Avatar from '@discourser/design-system';
|
|
907
|
+
|
|
908
|
+
test('displays image when loaded', async () => {
|
|
909
|
+
render(
|
|
910
|
+
<Avatar.Root colorPalette="primary">
|
|
911
|
+
<Avatar.Fallback name="John Doe" />
|
|
912
|
+
<Avatar.Image src="https://example.com/avatar.jpg" alt="John Doe" />
|
|
913
|
+
</Avatar.Root>
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const image = await screen.findByAltText('John Doe');
|
|
917
|
+
expect(image).toBeInTheDocument();
|
|
918
|
+
expect(image).toHaveAttribute('src', 'https://example.com/avatar.jpg');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('displays fallback when image fails', async () => {
|
|
922
|
+
render(
|
|
923
|
+
<Avatar.Root colorPalette="primary">
|
|
924
|
+
<Avatar.Fallback name="John Doe" />
|
|
925
|
+
<Avatar.Image src="invalid-url.jpg" alt="John Doe" />
|
|
926
|
+
</Avatar.Root>
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// Initially shows fallback
|
|
930
|
+
expect(screen.getByText('JD')).toBeInTheDocument();
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('generates correct initials from name', () => {
|
|
934
|
+
render(
|
|
935
|
+
<Avatar.Root colorPalette="primary">
|
|
936
|
+
<Avatar.Fallback name="Sarah Jane Williams" />
|
|
937
|
+
</Avatar.Root>
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
// Should show first and last name initials only
|
|
941
|
+
expect(screen.getByText('SW')).toBeInTheDocument();
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test('displays default icon when no name provided', () => {
|
|
945
|
+
const { container } = render(
|
|
946
|
+
<Avatar.Root colorPalette="primary">
|
|
947
|
+
<Avatar.Fallback />
|
|
948
|
+
</Avatar.Root>
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// Should render the default user icon SVG
|
|
952
|
+
expect(container.querySelector('svg')).toBeInTheDocument();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test('supports custom fallback content', () => {
|
|
956
|
+
render(
|
|
957
|
+
<Avatar.Root colorPalette="primary">
|
|
958
|
+
<Avatar.Fallback>🎉</Avatar.Fallback>
|
|
959
|
+
</Avatar.Root>
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
expect(screen.getByText('🎉')).toBeInTheDocument();
|
|
963
|
+
});
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
## Color Palette System
|
|
967
|
+
|
|
968
|
+
Avatar supports the design system's color palette:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
// Semantic colors
|
|
972
|
+
<Avatar.Root colorPalette="primary">
|
|
973
|
+
<Avatar.Fallback name="Primary User" />
|
|
974
|
+
</Avatar.Root>
|
|
975
|
+
|
|
976
|
+
<Avatar.Root colorPalette="neutral">
|
|
977
|
+
<Avatar.Fallback name="Neutral User" />
|
|
978
|
+
</Avatar.Root>
|
|
979
|
+
|
|
980
|
+
<Avatar.Root colorPalette="success">
|
|
981
|
+
<Avatar.Fallback name="Success" />
|
|
982
|
+
</Avatar.Root>
|
|
983
|
+
|
|
984
|
+
<Avatar.Root colorPalette="warning">
|
|
985
|
+
<Avatar.Fallback name="Warning" />
|
|
986
|
+
</Avatar.Root>
|
|
987
|
+
|
|
988
|
+
<Avatar.Root colorPalette="error">
|
|
989
|
+
<Avatar.Fallback name="Error" />
|
|
990
|
+
</Avatar.Root>
|
|
991
|
+
|
|
992
|
+
// Use different colors to categorize users
|
|
993
|
+
const getUserColor = (role: string) => {
|
|
994
|
+
const roleColors = {
|
|
995
|
+
admin: 'error',
|
|
996
|
+
moderator: 'warning',
|
|
997
|
+
premium: 'primary',
|
|
998
|
+
free: 'neutral',
|
|
999
|
+
};
|
|
1000
|
+
return roleColors[role] || 'neutral';
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
<Avatar.Root colorPalette={getUserColor(user.role)}>
|
|
1004
|
+
<Avatar.Fallback name={user.name} />
|
|
1005
|
+
<Avatar.Image src={user.avatar} alt={user.name} />
|
|
1006
|
+
</Avatar.Root>
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
## Related Components
|
|
1010
|
+
|
|
1011
|
+
- **Badge**: For displaying status or role labels near avatars
|
|
1012
|
+
- **Tooltip**: For showing additional user information on hover
|
|
1013
|
+
- **Dialog**: For displaying full user profiles
|
|
1014
|
+
- **IconButton**: For edit actions on avatars
|
|
1015
|
+
- **Skeleton**: For loading states
|