@ceed/ads 1.20.0 → 1.20.1-next.1
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/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/data-display/Markdown.md +832 -0
- package/dist/components/feedback/Dialog.md +605 -3
- package/dist/components/feedback/Modal.md +656 -24
- package/dist/components/feedback/llms.txt +1 -1
- package/dist/components/inputs/Autocomplete.md +734 -2
- package/dist/components/inputs/Calendar.md +655 -1
- package/dist/components/inputs/DatePicker.md +699 -3
- package/dist/components/inputs/DateRangePicker.md +815 -1
- package/dist/components/inputs/MonthPicker.md +626 -4
- package/dist/components/inputs/MonthRangePicker.md +682 -4
- package/dist/components/inputs/Select.md +600 -0
- package/dist/components/layout/Container.md +507 -0
- package/dist/components/navigation/Breadcrumbs.md +582 -0
- package/dist/components/navigation/IconMenuButton.md +693 -0
- package/dist/components/navigation/InsetDrawer.md +1150 -3
- package/dist/components/navigation/Link.md +526 -0
- package/dist/components/navigation/MenuButton.md +632 -0
- package/dist/components/navigation/NavigationGroup.md +401 -1
- package/dist/components/navigation/NavigationItem.md +311 -0
- package/dist/components/navigation/Navigator.md +373 -0
- package/dist/components/navigation/Pagination.md +521 -0
- package/dist/components/navigation/ProfileMenu.md +605 -0
- package/dist/components/navigation/Tabs.md +609 -7
- package/dist/components/surfaces/Accordions.md +947 -3
- package/dist/index.cjs +3 -1
- package/dist/index.js +3 -1
- package/dist/llms.txt +1 -1
- package/framer/index.js +1 -1
- package/package.json +3 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
4
4
|
|
|
5
|
+
ProfileMenu is a user account dropdown component that displays the current user's profile information and provides quick access to account-related actions. It combines an avatar button with a dropdown menu containing the user's name, optional badge (chip), caption (like email), and customizable menu items. ProfileMenu is typically placed in the header or navigation area of an application.
|
|
6
|
+
|
|
5
7
|
```tsx
|
|
6
8
|
<ProfileMenu
|
|
7
9
|
open
|
|
@@ -31,4 +33,607 @@
|
|
|
31
33
|
|
|
32
34
|
```tsx
|
|
33
35
|
import { ProfileMenu } from '@ceed/ads';
|
|
36
|
+
|
|
37
|
+
function Header() {
|
|
38
|
+
return (
|
|
39
|
+
<ProfileMenu
|
|
40
|
+
profile={{
|
|
41
|
+
name: 'John Doe',
|
|
42
|
+
caption: 'john@example.com',
|
|
43
|
+
chip: 'Admin',
|
|
44
|
+
}}
|
|
45
|
+
menuItems={[
|
|
46
|
+
{ label: 'Profile', onClick: () => navigate('/profile') },
|
|
47
|
+
{ label: 'Settings', onClick: () => navigate('/settings') },
|
|
48
|
+
{ label: 'Sign Out', onClick: () => signOut() },
|
|
49
|
+
]}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
### Default
|
|
58
|
+
|
|
59
|
+
ProfileMenu with full profile information including name, chip, and caption.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<ProfileMenu
|
|
63
|
+
open
|
|
64
|
+
profile={{
|
|
65
|
+
name: 'John Gordon',
|
|
66
|
+
chip: 'PDT',
|
|
67
|
+
caption: 'j.gordon@haulla.com'
|
|
68
|
+
}}
|
|
69
|
+
menuItems={[{
|
|
70
|
+
label: 'Menu Item1',
|
|
71
|
+
onClick: fn()
|
|
72
|
+
}, {
|
|
73
|
+
label: 'Menu Item2',
|
|
74
|
+
onClick: fn()
|
|
75
|
+
}, {
|
|
76
|
+
label: 'Menu Item3',
|
|
77
|
+
onClick: fn()
|
|
78
|
+
}]}
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Without Caption
|
|
83
|
+
|
|
84
|
+
Profile showing only the name and chip badge.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<ProfileMenu
|
|
88
|
+
open
|
|
89
|
+
profile={{
|
|
90
|
+
name: 'John Gordon',
|
|
91
|
+
chip: 'PDT'
|
|
92
|
+
}}
|
|
93
|
+
menuItems={[{
|
|
94
|
+
label: 'Menu Item1',
|
|
95
|
+
onClick: fn()
|
|
96
|
+
}, {
|
|
97
|
+
label: 'Menu Item2',
|
|
98
|
+
onClick: fn()
|
|
99
|
+
}, {
|
|
100
|
+
label: 'Menu Item3',
|
|
101
|
+
onClick: fn()
|
|
102
|
+
}]}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Without Chip
|
|
107
|
+
|
|
108
|
+
Profile showing name and caption without a badge.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<ProfileMenu
|
|
112
|
+
open
|
|
113
|
+
profile={{
|
|
114
|
+
name: 'John Gordon',
|
|
115
|
+
caption: 'j.gordon@haulla.com'
|
|
116
|
+
}}
|
|
117
|
+
menuItems={[{
|
|
118
|
+
label: 'Menu Item1',
|
|
119
|
+
onClick: fn()
|
|
120
|
+
}, {
|
|
121
|
+
label: 'Menu Item2',
|
|
122
|
+
onClick: fn()
|
|
123
|
+
}, {
|
|
124
|
+
label: 'Menu Item3',
|
|
125
|
+
onClick: fn()
|
|
126
|
+
}]}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Only Name
|
|
131
|
+
|
|
132
|
+
Minimal profile with just the user's name.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<ProfileMenu
|
|
136
|
+
open
|
|
137
|
+
profile={{
|
|
138
|
+
name: 'John Gordon'
|
|
139
|
+
}}
|
|
140
|
+
menuItems={[{
|
|
141
|
+
label: 'Menu Item1',
|
|
142
|
+
onClick: fn()
|
|
143
|
+
}, {
|
|
144
|
+
label: 'Menu Item2',
|
|
145
|
+
onClick: fn()
|
|
146
|
+
}, {
|
|
147
|
+
label: 'Menu Item3',
|
|
148
|
+
onClick: fn()
|
|
149
|
+
}]}
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Without Menu Items
|
|
154
|
+
|
|
155
|
+
Profile card without any menu actions.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<ProfileMenu
|
|
159
|
+
open
|
|
160
|
+
profile={{
|
|
161
|
+
name: 'John Gordon',
|
|
162
|
+
chip: 'PDT',
|
|
163
|
+
caption: 'j.gordon@haulla.com'
|
|
164
|
+
}}
|
|
165
|
+
menuItems={[]}
|
|
166
|
+
/>
|
|
34
167
|
```
|
|
168
|
+
|
|
169
|
+
### Sizes
|
|
170
|
+
|
|
171
|
+
ProfileMenu supports different sizes for various layouts.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<Stack direction="row" gap="150px">
|
|
175
|
+
<ProfileMenu {...args} size="md" profile={{
|
|
176
|
+
name: 'John Gordon',
|
|
177
|
+
chip: 'PDT',
|
|
178
|
+
caption: 'j.gordon@haulla.com'
|
|
179
|
+
}} menuItems={[{
|
|
180
|
+
label: 'Menu Item1',
|
|
181
|
+
onClick: fn()
|
|
182
|
+
}, {
|
|
183
|
+
label: 'Menu Item2',
|
|
184
|
+
onClick: fn()
|
|
185
|
+
}, {
|
|
186
|
+
label: 'Menu Item3',
|
|
187
|
+
onClick: fn()
|
|
188
|
+
}]} />
|
|
189
|
+
<ProfileMenu {...args} size="sm" profile={{
|
|
190
|
+
name: 'John Gordon',
|
|
191
|
+
chip: 'PDT',
|
|
192
|
+
caption: 'j.gordon@haulla.com'
|
|
193
|
+
}} menuItems={[{
|
|
194
|
+
label: 'Menu Item1',
|
|
195
|
+
onClick: fn()
|
|
196
|
+
}, {
|
|
197
|
+
label: 'Menu Item2',
|
|
198
|
+
onClick: fn()
|
|
199
|
+
}, {
|
|
200
|
+
label: 'Menu Item3',
|
|
201
|
+
onClick: fn()
|
|
202
|
+
}]} />
|
|
203
|
+
</Stack>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### With Profile Image
|
|
207
|
+
|
|
208
|
+
Display a custom avatar image instead of generated initials.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<ProfileMenu
|
|
212
|
+
profile={{
|
|
213
|
+
name: 'John Gordon',
|
|
214
|
+
chip: 'PDT',
|
|
215
|
+
caption: 'j.gordon@haulla.com',
|
|
216
|
+
image: {
|
|
217
|
+
src: 'https://i.pravatar.cc/300?u=test_profile',
|
|
218
|
+
alt: 'User Avatar'
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
menuItems={[{
|
|
222
|
+
label: 'Menu Item1',
|
|
223
|
+
onClick: fn()
|
|
224
|
+
}, {
|
|
225
|
+
label: 'Menu Item2',
|
|
226
|
+
onClick: fn()
|
|
227
|
+
}, {
|
|
228
|
+
label: 'Menu Item3',
|
|
229
|
+
onClick: fn()
|
|
230
|
+
}]}
|
|
231
|
+
/>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### With Korean Name
|
|
235
|
+
|
|
236
|
+
ProfileMenu automatically generates initials from various name formats.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<>
|
|
240
|
+
<ProfileMenu {...args} profile={{
|
|
241
|
+
name: '홍길동',
|
|
242
|
+
chip: 'PDT',
|
|
243
|
+
caption: 'j.gordon@haulla.com'
|
|
244
|
+
}} />
|
|
245
|
+
<ProfileMenu {...args} profile={{
|
|
246
|
+
name: '제갈공명',
|
|
247
|
+
chip: 'PDT',
|
|
248
|
+
caption: 'j.gordon@haulla.com'
|
|
249
|
+
}} />
|
|
250
|
+
</>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Controlled
|
|
254
|
+
|
|
255
|
+
Programmatically control the menu's open state.
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
<ProfileMenu {...args} open={open} onOpenChange={setOpen} />
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Uncontrolled
|
|
262
|
+
|
|
263
|
+
Let the component manage its own open state internally.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<ProfileMenu
|
|
267
|
+
defaultOpen={false}
|
|
268
|
+
profile={{
|
|
269
|
+
name: 'John Gordon',
|
|
270
|
+
chip: 'PDT',
|
|
271
|
+
caption: 'j.gordon@haulla.com'
|
|
272
|
+
}}
|
|
273
|
+
menuItems={[{
|
|
274
|
+
label: 'Menu Item1',
|
|
275
|
+
onClick: fn()
|
|
276
|
+
}, {
|
|
277
|
+
label: 'Menu Item2',
|
|
278
|
+
onClick: fn()
|
|
279
|
+
}, {
|
|
280
|
+
label: 'Menu Item3',
|
|
281
|
+
onClick: fn()
|
|
282
|
+
}]}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## When to Use
|
|
287
|
+
|
|
288
|
+
### ✅ Good Use Cases
|
|
289
|
+
|
|
290
|
+
- **Application header**: Display current user and account actions
|
|
291
|
+
- **Admin dashboards**: Quick access to user settings and logout
|
|
292
|
+
- **Multi-user applications**: Show which account is active
|
|
293
|
+
- **Settings access**: Provide shortcuts to profile and preferences
|
|
294
|
+
- **Authentication flows**: Display logged-in user with sign-out option
|
|
295
|
+
|
|
296
|
+
### ❌ When Not to Use
|
|
297
|
+
|
|
298
|
+
- **Anonymous users**: Don't show ProfileMenu for non-authenticated users
|
|
299
|
+
- **Simple navigation**: Use regular dropdown for non-profile menus
|
|
300
|
+
- **Primary actions**: Critical actions belong in the main UI, not hidden in profile
|
|
301
|
+
- **Complex forms**: Don't put forms or heavy content in the dropdown
|
|
302
|
+
- **Mobile navigation**: Consider slide-out drawer or full-screen menu instead
|
|
303
|
+
|
|
304
|
+
## Common Use Cases
|
|
305
|
+
|
|
306
|
+
### Header Navigation
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
function AppHeader() {
|
|
310
|
+
const { user, signOut } = useAuth();
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<header>
|
|
314
|
+
<Logo />
|
|
315
|
+
<Navigation />
|
|
316
|
+
<ProfileMenu
|
|
317
|
+
profile={{
|
|
318
|
+
name: user.name,
|
|
319
|
+
caption: user.email,
|
|
320
|
+
chip: user.role,
|
|
321
|
+
image: user.avatar ? { src: user.avatar, alt: user.name } : undefined,
|
|
322
|
+
}}
|
|
323
|
+
menuItems={[
|
|
324
|
+
{ label: 'My Profile', onClick: () => navigate('/profile') },
|
|
325
|
+
{ label: 'Account Settings', onClick: () => navigate('/settings') },
|
|
326
|
+
{ label: 'Help & Support', onClick: () => window.open('/help') },
|
|
327
|
+
{ label: 'Sign Out', onClick: signOut },
|
|
328
|
+
]}
|
|
329
|
+
/>
|
|
330
|
+
</header>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Multi-Role User
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
function RoleBasedProfileMenu({ user, currentRole, switchRole }) {
|
|
339
|
+
const menuItems = [
|
|
340
|
+
{ label: 'My Profile', onClick: () => navigate('/profile') },
|
|
341
|
+
{ label: 'Settings', onClick: () => navigate('/settings') },
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
// Add role switching if user has multiple roles
|
|
345
|
+
if (user.roles.length > 1) {
|
|
346
|
+
user.roles
|
|
347
|
+
.filter((role) => role !== currentRole)
|
|
348
|
+
.forEach((role) => {
|
|
349
|
+
menuItems.push({
|
|
350
|
+
label: `Switch to ${role}`,
|
|
351
|
+
onClick: () => switchRole(role),
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
menuItems.push({ label: 'Sign Out', onClick: () => signOut() });
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<ProfileMenu
|
|
360
|
+
profile={{
|
|
361
|
+
name: user.name,
|
|
362
|
+
caption: user.email,
|
|
363
|
+
chip: currentRole,
|
|
364
|
+
}}
|
|
365
|
+
menuItems={menuItems}
|
|
366
|
+
/>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Organization Context
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
function OrgProfileMenu({ user, organization }) {
|
|
375
|
+
return (
|
|
376
|
+
<ProfileMenu
|
|
377
|
+
profile={{
|
|
378
|
+
name: user.name,
|
|
379
|
+
caption: organization.name,
|
|
380
|
+
chip: user.organizationRole,
|
|
381
|
+
image: user.avatar ? { src: user.avatar, alt: user.name } : undefined,
|
|
382
|
+
}}
|
|
383
|
+
menuItems={[
|
|
384
|
+
{ label: 'My Account', onClick: () => navigate('/account') },
|
|
385
|
+
{ label: 'Organization Settings', onClick: () => navigate('/org/settings') },
|
|
386
|
+
{ label: 'Switch Organization', onClick: () => openOrgSwitcher() },
|
|
387
|
+
{ label: 'Sign Out', onClick: () => signOut() },
|
|
388
|
+
]}
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### With Custom Initial Generation
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
function CustomInitialsProfileMenu({ user }) {
|
|
398
|
+
// Custom function to generate initials
|
|
399
|
+
const getInitial = (name) => {
|
|
400
|
+
// For Korean names, use first character
|
|
401
|
+
if (/[\uAC00-\uD7AF]/.test(name)) {
|
|
402
|
+
return name.charAt(0);
|
|
403
|
+
}
|
|
404
|
+
// For English names, use first letters of first and last name
|
|
405
|
+
const parts = name.split(' ');
|
|
406
|
+
if (parts.length >= 2) {
|
|
407
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`;
|
|
408
|
+
}
|
|
409
|
+
return name.substring(0, 2).toUpperCase();
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<ProfileMenu
|
|
414
|
+
profile={{
|
|
415
|
+
name: user.name,
|
|
416
|
+
caption: user.email,
|
|
417
|
+
}}
|
|
418
|
+
menuItems={[
|
|
419
|
+
{ label: 'Profile', onClick: () => navigate('/profile') },
|
|
420
|
+
{ label: 'Sign Out', onClick: () => signOut() },
|
|
421
|
+
]}
|
|
422
|
+
getInitial={getInitial}
|
|
423
|
+
/>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Props and Customization
|
|
429
|
+
|
|
430
|
+
### Key Props
|
|
431
|
+
|
|
432
|
+
| Prop | Type | Default | Description |
|
|
433
|
+
| -------------- | -------------------------- | ------- | ---------------------------------- |
|
|
434
|
+
| `profile` | `Profile` | - | User profile information |
|
|
435
|
+
| `menuItems` | `MenuItem[]` | `[]` | Array of menu actions |
|
|
436
|
+
| `open` | `boolean` | - | Controlled open state |
|
|
437
|
+
| `defaultOpen` | `boolean` | `false` | Default open state (uncontrolled) |
|
|
438
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
|
|
439
|
+
| `size` | `'sm' \| 'md'` | `'md'` | Size variant |
|
|
440
|
+
| `getInitial` | `(name: string) => string` | - | Custom initial generation function |
|
|
441
|
+
|
|
442
|
+
### Profile Type
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
interface Profile {
|
|
446
|
+
name: string; // User's display name (required)
|
|
447
|
+
caption?: string; // Secondary text (email, role description)
|
|
448
|
+
chip?: string; // Badge text (role, status)
|
|
449
|
+
image?: {
|
|
450
|
+
src: string; // Avatar image URL
|
|
451
|
+
alt: string; // Alt text for accessibility
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### MenuItem Type
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
interface MenuItem {
|
|
460
|
+
label: string; // Menu item text
|
|
461
|
+
onClick?: () => void; // Click handler
|
|
462
|
+
// Plus any other MenuItem props from Joy UI
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Controlled vs Uncontrolled
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
// Uncontrolled - component manages state
|
|
470
|
+
<ProfileMenu
|
|
471
|
+
defaultOpen={false}
|
|
472
|
+
profile={profile}
|
|
473
|
+
menuItems={menuItems}
|
|
474
|
+
/>
|
|
475
|
+
|
|
476
|
+
// Controlled - you manage state
|
|
477
|
+
const [open, setOpen] = useState(false);
|
|
478
|
+
<ProfileMenu
|
|
479
|
+
open={open}
|
|
480
|
+
onOpenChange={setOpen}
|
|
481
|
+
profile={profile}
|
|
482
|
+
menuItems={menuItems}
|
|
483
|
+
/>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Accessibility
|
|
487
|
+
|
|
488
|
+
ProfileMenu includes built-in accessibility features:
|
|
489
|
+
|
|
490
|
+
### ARIA Attributes
|
|
491
|
+
|
|
492
|
+
- Button has proper `aria-haspopup` and `aria-expanded` attributes
|
|
493
|
+
- Menu uses `role="menu"` with proper `role="menuitem"` for items
|
|
494
|
+
- Focus management when opening and closing
|
|
495
|
+
|
|
496
|
+
### Keyboard Navigation
|
|
497
|
+
|
|
498
|
+
- **Tab**: Focus the profile button
|
|
499
|
+
- **Enter/Space**: Open the menu
|
|
500
|
+
- **Arrow Down**: Move to next menu item
|
|
501
|
+
- **Arrow Up**: Move to previous menu item
|
|
502
|
+
- **Enter**: Activate focused menu item
|
|
503
|
+
- **Escape**: Close the menu
|
|
504
|
+
|
|
505
|
+
### Screen Reader Support
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
// Avatar announces the user name
|
|
509
|
+
<ProfileMenu
|
|
510
|
+
profile={{
|
|
511
|
+
name: 'John Doe', // Avatar reads "JD, John Doe"
|
|
512
|
+
image: { src: '...', alt: 'John Doe profile picture' },
|
|
513
|
+
}}
|
|
514
|
+
/>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Focus Management
|
|
518
|
+
|
|
519
|
+
- Focus moves to first menu item when opened
|
|
520
|
+
- Focus returns to button when menu closes
|
|
521
|
+
- Click outside closes menu and returns focus
|
|
522
|
+
|
|
523
|
+
## Best Practices
|
|
524
|
+
|
|
525
|
+
### ✅ Do
|
|
526
|
+
|
|
527
|
+
1. **Include essential actions**: Always include profile and sign-out options
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
// ✅ Good: Essential menu items
|
|
531
|
+
<ProfileMenu
|
|
532
|
+
menuItems={[
|
|
533
|
+
{ label: 'Profile', onClick: handleProfile },
|
|
534
|
+
{ label: 'Settings', onClick: handleSettings },
|
|
535
|
+
{ label: 'Sign Out', onClick: handleSignOut },
|
|
536
|
+
]}
|
|
537
|
+
/>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
2. **Show user context**: Display role or organization context in the chip
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
// ✅ Good: Clear user context
|
|
544
|
+
<ProfileMenu
|
|
545
|
+
profile={{
|
|
546
|
+
name: 'Jane Smith',
|
|
547
|
+
caption: 'jane@company.com',
|
|
548
|
+
chip: 'Admin', // Shows current role
|
|
549
|
+
}}
|
|
550
|
+
/>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
3. **Use descriptive labels**: Menu items should clearly describe actions
|
|
554
|
+
|
|
555
|
+
4. **Provide avatar images**: When available, show actual profile photos
|
|
556
|
+
|
|
557
|
+
### ❌ Don't
|
|
558
|
+
|
|
559
|
+
1. **Don't overload with items**: Keep menu items to 5-7 maximum
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
// ❌ Bad: Too many items
|
|
563
|
+
<ProfileMenu
|
|
564
|
+
menuItems={[
|
|
565
|
+
{ label: 'Profile' },
|
|
566
|
+
{ label: 'Settings' },
|
|
567
|
+
{ label: 'Billing' },
|
|
568
|
+
{ label: 'Team' },
|
|
569
|
+
{ label: 'Integrations' },
|
|
570
|
+
{ label: 'API Keys' },
|
|
571
|
+
{ label: 'Audit Log' },
|
|
572
|
+
{ label: 'Help' },
|
|
573
|
+
{ label: 'Sign Out' },
|
|
574
|
+
]}
|
|
575
|
+
/>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
2. **Don't hide critical actions**: Important actions should be in main navigation
|
|
579
|
+
|
|
580
|
+
3. **Don't use for non-user content**: ProfileMenu is specifically for user accounts
|
|
581
|
+
|
|
582
|
+
4. **Don't leave profile empty**: Always provide at least the name
|
|
583
|
+
|
|
584
|
+
## Performance Considerations
|
|
585
|
+
|
|
586
|
+
### Lazy Load Menu Content
|
|
587
|
+
|
|
588
|
+
For dynamic menu items, fetch data when needed:
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
function ProfileMenuWithDynamicItems({ user }) {
|
|
592
|
+
const [menuItems, setMenuItems] = useState(baseMenuItems);
|
|
593
|
+
|
|
594
|
+
const handleOpenChange = async (isOpen) => {
|
|
595
|
+
if (isOpen && !menuItems.includes(dynamicItems)) {
|
|
596
|
+
const items = await fetchUserMenuItems(user.id);
|
|
597
|
+
setMenuItems([...baseMenuItems, ...items]);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
return (
|
|
602
|
+
<ProfileMenu
|
|
603
|
+
profile={user}
|
|
604
|
+
menuItems={menuItems}
|
|
605
|
+
onOpenChange={handleOpenChange}
|
|
606
|
+
/>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Memoize Menu Items
|
|
612
|
+
|
|
613
|
+
When menu items are computed, memoize them:
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
const menuItems = useMemo(() => [
|
|
617
|
+
{ label: 'Profile', onClick: handleProfile },
|
|
618
|
+
{ label: 'Settings', onClick: handleSettings },
|
|
619
|
+
...(user.isAdmin ? [{ label: 'Admin', onClick: handleAdmin }] : []),
|
|
620
|
+
{ label: 'Sign Out', onClick: handleSignOut },
|
|
621
|
+
], [user.isAdmin, handleProfile, handleSettings, handleAdmin, handleSignOut]);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Optimize Avatar Loading
|
|
625
|
+
|
|
626
|
+
For profile images, ensure proper caching and loading:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
<ProfileMenu
|
|
630
|
+
profile={{
|
|
631
|
+
name: user.name,
|
|
632
|
+
image: user.avatarUrl
|
|
633
|
+
? { src: `${user.avatarUrl}?size=64`, alt: user.name }
|
|
634
|
+
: undefined,
|
|
635
|
+
}}
|
|
636
|
+
/>
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
ProfileMenu provides a polished user account interface for admin applications. Place it in your header for easy access to user-related actions and account management.
|