@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.
Files changed (30) hide show
  1. package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
  2. package/dist/components/data-display/Markdown.md +832 -0
  3. package/dist/components/feedback/Dialog.md +605 -3
  4. package/dist/components/feedback/Modal.md +656 -24
  5. package/dist/components/feedback/llms.txt +1 -1
  6. package/dist/components/inputs/Autocomplete.md +734 -2
  7. package/dist/components/inputs/Calendar.md +655 -1
  8. package/dist/components/inputs/DatePicker.md +699 -3
  9. package/dist/components/inputs/DateRangePicker.md +815 -1
  10. package/dist/components/inputs/MonthPicker.md +626 -4
  11. package/dist/components/inputs/MonthRangePicker.md +682 -4
  12. package/dist/components/inputs/Select.md +600 -0
  13. package/dist/components/layout/Container.md +507 -0
  14. package/dist/components/navigation/Breadcrumbs.md +582 -0
  15. package/dist/components/navigation/IconMenuButton.md +693 -0
  16. package/dist/components/navigation/InsetDrawer.md +1150 -3
  17. package/dist/components/navigation/Link.md +526 -0
  18. package/dist/components/navigation/MenuButton.md +632 -0
  19. package/dist/components/navigation/NavigationGroup.md +401 -1
  20. package/dist/components/navigation/NavigationItem.md +311 -0
  21. package/dist/components/navigation/Navigator.md +373 -0
  22. package/dist/components/navigation/Pagination.md +521 -0
  23. package/dist/components/navigation/ProfileMenu.md +605 -0
  24. package/dist/components/navigation/Tabs.md +609 -7
  25. package/dist/components/surfaces/Accordions.md +947 -3
  26. package/dist/index.cjs +3 -1
  27. package/dist/index.js +3 -1
  28. package/dist/llms.txt +1 -1
  29. package/framer/index.js +1 -1
  30. 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.