@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.2.30 → 0.2.32
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/CHANGELOG.md +1 -1
- package/dist/components/ui/navigation/index.d.ts +4 -1
- package/dist/components/ui/navigation/index.d.ts.map +1 -1
- package/dist/components/ui/navigation/index.esm.js +4 -0
- package/dist/components/ui/navigation/index.js +4 -0
- package/dist/components/ui/navigation/subscription-badge.d.ts +9 -0
- package/dist/components/ui/navigation/subscription-badge.d.ts.map +1 -0
- package/dist/components/ui/navigation/subscription-badge.esm.js +58 -0
- package/dist/components/ui/navigation/subscription-badge.js +58 -0
- package/dist/components/ui/navigation/types.d.ts +1 -0
- package/dist/components/ui/navigation/types.d.ts.map +1 -1
- package/dist/components/ui/navigation/user-avatar.d.ts +9 -0
- package/dist/components/ui/navigation/user-avatar.d.ts.map +1 -0
- package/dist/components/ui/navigation/user-avatar.esm.js +55 -0
- package/dist/components/ui/navigation/user-avatar.js +55 -0
- package/dist/components/ui/navigation/user-menu-examples.d.ts +8 -0
- package/dist/components/ui/navigation/user-menu-examples.d.ts.map +1 -0
- package/dist/components/ui/navigation/user-menu-examples.esm.js +125 -0
- package/dist/components/ui/navigation/user-menu-examples.js +125 -0
- package/dist/components/ui/navigation/user-menu-types.d.ts +218 -0
- package/dist/components/ui/navigation/user-menu-types.d.ts.map +1 -0
- package/dist/components/ui/navigation/user-menu-types.esm.js +5 -0
- package/dist/components/ui/navigation/user-menu-types.js +5 -0
- package/dist/components/ui/navigation/user-menu.d.ts +9 -0
- package/dist/components/ui/navigation/user-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation/user-menu.esm.js +154 -0
- package/dist/components/ui/navigation/user-menu.js +154 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +5 -1
- package/dist/index.js +5 -1
- package/dist/styles.css +2 -2
- package/package.json +1 -1
- package/src/components/ui/navigation/index.ts +13 -0
- package/src/components/ui/navigation/subscription-badge.tsx +110 -0
- package/src/components/ui/navigation/types.ts +14 -0
- package/src/components/ui/navigation/user-avatar.tsx +111 -0
- package/src/components/ui/navigation/user-menu-examples.tsx +551 -0
- package/src/components/ui/navigation/user-menu-types.ts +308 -0
- package/src/components/ui/navigation/user-menu.tsx +354 -0
- package/src/index.ts +5 -1
- package/src/styles/components/navigation/user-menu.css +525 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserMenu Component Types
|
|
3
|
+
* Comprehensive type definitions for the UserMenu component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ReactNode } from 'react';
|
|
7
|
+
import { ButtonProps } from '../button.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CORE USER MENU TYPES
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface UserMenuUser {
|
|
14
|
+
fullName?: string;
|
|
15
|
+
firstName?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
avatar?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UserMenuSubscription {
|
|
21
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UserMenuItem {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
icon?: ReactNode;
|
|
30
|
+
href?: string; // For navigation
|
|
31
|
+
onClick?: () => void; // For actions
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
variant?: 'default' | 'danger'; // For sign out styling
|
|
34
|
+
badge?: ReactNode;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface UserMenuGroup {
|
|
39
|
+
id: string;
|
|
40
|
+
title?: string; // Optional group title
|
|
41
|
+
divider?: boolean; // Show divider above group
|
|
42
|
+
items: UserMenuItem[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// MAIN COMPONENT PROPS
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export interface UserMenuProps {
|
|
50
|
+
// User Information
|
|
51
|
+
user: UserMenuUser;
|
|
52
|
+
|
|
53
|
+
// Subscription/Plan Information
|
|
54
|
+
subscription?: UserMenuSubscription;
|
|
55
|
+
|
|
56
|
+
// Custom Trigger Support
|
|
57
|
+
trigger?: ReactNode; // Custom trigger element
|
|
58
|
+
triggerProps?: ButtonProps; // Default trigger customization
|
|
59
|
+
|
|
60
|
+
// Menu Configuration
|
|
61
|
+
groups: UserMenuGroup[];
|
|
62
|
+
|
|
63
|
+
// Header Configuration
|
|
64
|
+
showHeader?: boolean; // Show user header section
|
|
65
|
+
headerCustom?: ReactNode; // Custom header override
|
|
66
|
+
|
|
67
|
+
// Positioning & Behavior
|
|
68
|
+
placement?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
69
|
+
offset?: number;
|
|
70
|
+
width?: number | string;
|
|
71
|
+
maxHeight?: number | string;
|
|
72
|
+
|
|
73
|
+
// Interaction Callbacks
|
|
74
|
+
onItemClick?: (item: UserMenuItem) => void;
|
|
75
|
+
onSignOut?: () => void;
|
|
76
|
+
|
|
77
|
+
// State Management
|
|
78
|
+
open?: boolean;
|
|
79
|
+
defaultOpen?: boolean;
|
|
80
|
+
onOpenChange?: (open: boolean) => void;
|
|
81
|
+
|
|
82
|
+
// Styling
|
|
83
|
+
className?: string;
|
|
84
|
+
headerClassName?: string;
|
|
85
|
+
dropdownClassName?: string;
|
|
86
|
+
|
|
87
|
+
// Accessibility
|
|
88
|
+
'aria-label'?: string;
|
|
89
|
+
'data-testid'?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// COMPONENT STATE TYPES
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
export interface UserMenuState {
|
|
97
|
+
isOpen: boolean;
|
|
98
|
+
focusedIndex: number;
|
|
99
|
+
searchQuery: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type UserMenuAction =
|
|
103
|
+
| { type: 'TOGGLE_MENU' }
|
|
104
|
+
| { type: 'OPEN_MENU' }
|
|
105
|
+
| { type: 'CLOSE_MENU' }
|
|
106
|
+
| { type: 'SET_FOCUSED_INDEX'; payload: number }
|
|
107
|
+
| { type: 'SET_SEARCH_QUERY'; payload: string }
|
|
108
|
+
| { type: 'RESET_FOCUS' };
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// SUB-COMPONENT PROPS
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export interface UserMenuTriggerProps {
|
|
115
|
+
user: UserMenuUser;
|
|
116
|
+
subscription?: UserMenuSubscription;
|
|
117
|
+
isOpen: boolean;
|
|
118
|
+
onToggle: () => void;
|
|
119
|
+
disabled?: boolean;
|
|
120
|
+
className?: string;
|
|
121
|
+
children?: ReactNode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface UserMenuDropdownProps {
|
|
125
|
+
user: UserMenuUser;
|
|
126
|
+
subscription?: UserMenuSubscription;
|
|
127
|
+
groups: UserMenuGroup[];
|
|
128
|
+
showHeader?: boolean;
|
|
129
|
+
headerCustom?: ReactNode;
|
|
130
|
+
placement: string;
|
|
131
|
+
width?: number | string;
|
|
132
|
+
maxHeight?: number | string;
|
|
133
|
+
onItemClick?: (item: UserMenuItem) => void;
|
|
134
|
+
onClose: () => void;
|
|
135
|
+
className?: string;
|
|
136
|
+
headerClassName?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface UserMenuHeaderProps {
|
|
140
|
+
user: UserMenuUser;
|
|
141
|
+
subscription?: UserMenuSubscription;
|
|
142
|
+
className?: string;
|
|
143
|
+
custom?: ReactNode;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface UserMenuSectionProps {
|
|
147
|
+
group: UserMenuGroup;
|
|
148
|
+
onItemClick?: (item: UserMenuItem) => void;
|
|
149
|
+
focusedIndex?: number;
|
|
150
|
+
sectionIndex: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface UserMenuItemProps {
|
|
154
|
+
item: UserMenuItem;
|
|
155
|
+
onItemClick?: (item: UserMenuItem) => void;
|
|
156
|
+
focused?: boolean;
|
|
157
|
+
itemIndex: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// AVATAR COMPONENT TYPES
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
export interface UserAvatarProps {
|
|
165
|
+
user: UserMenuUser;
|
|
166
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
167
|
+
className?: string;
|
|
168
|
+
fallbackClassName?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// SUBSCRIPTION BADGE TYPES
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
export interface SubscriptionBadgeProps {
|
|
176
|
+
subscription: UserMenuSubscription;
|
|
177
|
+
size?: 'sm' | 'md' | 'lg';
|
|
178
|
+
variant?: 'default' | 'outline';
|
|
179
|
+
showIcon?: boolean;
|
|
180
|
+
className?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// UTILITY TYPES
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
export interface UserMenuPlacement {
|
|
188
|
+
top?: number;
|
|
189
|
+
bottom?: number;
|
|
190
|
+
left?: number;
|
|
191
|
+
right?: number;
|
|
192
|
+
transform?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface UserMenuDimensions {
|
|
196
|
+
width: number;
|
|
197
|
+
height: number;
|
|
198
|
+
triggerRect: DOMRect;
|
|
199
|
+
dropdownRect: DOMRect;
|
|
200
|
+
viewportWidth: number;
|
|
201
|
+
viewportHeight: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface UserMenuAccessibility {
|
|
205
|
+
role: string;
|
|
206
|
+
'aria-expanded': boolean;
|
|
207
|
+
'aria-haspopup': boolean;
|
|
208
|
+
'aria-label': string;
|
|
209
|
+
'aria-labelledby'?: string;
|
|
210
|
+
'aria-describedby'?: string;
|
|
211
|
+
tabIndex: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// ANIMATION TYPES
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
export interface UserMenuAnimationConfig {
|
|
219
|
+
enter: {
|
|
220
|
+
duration: number;
|
|
221
|
+
easing: string;
|
|
222
|
+
from: Record<string, any>;
|
|
223
|
+
to: Record<string, any>;
|
|
224
|
+
};
|
|
225
|
+
exit: {
|
|
226
|
+
duration: number;
|
|
227
|
+
easing: string;
|
|
228
|
+
from: Record<string, any>;
|
|
229
|
+
to: Record<string, any>;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// RESPONSIVE TYPES
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
export interface UserMenuBreakpoints {
|
|
238
|
+
mobile: number;
|
|
239
|
+
tablet: number;
|
|
240
|
+
desktop: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface UserMenuResponsiveConfig {
|
|
244
|
+
mobile: Partial<UserMenuProps>;
|
|
245
|
+
tablet: Partial<UserMenuProps>;
|
|
246
|
+
desktop: Partial<UserMenuProps>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// TESTING TYPES
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
export interface UserMenuTestProps {
|
|
254
|
+
'data-testid'?: string;
|
|
255
|
+
'data-test-trigger'?: string;
|
|
256
|
+
'data-test-dropdown'?: string;
|
|
257
|
+
'data-test-header'?: string;
|
|
258
|
+
'data-test-section'?: string;
|
|
259
|
+
'data-test-item'?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// THEME INTEGRATION TYPES
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
export interface UserMenuThemeConfig {
|
|
267
|
+
colors: {
|
|
268
|
+
background: string;
|
|
269
|
+
border: string;
|
|
270
|
+
text: string;
|
|
271
|
+
textMuted: string;
|
|
272
|
+
hover: string;
|
|
273
|
+
focus: string;
|
|
274
|
+
danger: string;
|
|
275
|
+
};
|
|
276
|
+
spacing: {
|
|
277
|
+
padding: string;
|
|
278
|
+
gap: string;
|
|
279
|
+
offset: string;
|
|
280
|
+
};
|
|
281
|
+
typography: {
|
|
282
|
+
fontSize: string;
|
|
283
|
+
fontWeight: string;
|
|
284
|
+
lineHeight: string;
|
|
285
|
+
};
|
|
286
|
+
shadows: {
|
|
287
|
+
dropdown: string;
|
|
288
|
+
focus: string;
|
|
289
|
+
};
|
|
290
|
+
transitions: {
|
|
291
|
+
duration: string;
|
|
292
|
+
easing: string;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// EXPORT ALL TYPES
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
export type {
|
|
301
|
+
UserMenuUser as User,
|
|
302
|
+
UserMenuSubscription as Subscription,
|
|
303
|
+
UserMenuItem as MenuItem,
|
|
304
|
+
UserMenuGroup as MenuGroup,
|
|
305
|
+
UserMenuProps as Props,
|
|
306
|
+
UserMenuState as State,
|
|
307
|
+
UserMenuAction as Action,
|
|
308
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserMenu Component
|
|
3
|
+
* Comprehensive user menu with custom trigger, user header, and grouped menu items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
10
|
+
import { UserMenuProps, UserMenuItem, UserMenuGroup } from './user-menu-types.js';
|
|
11
|
+
import { Button } from '../button.js';
|
|
12
|
+
import { UserAvatar } from './user-avatar.js';
|
|
13
|
+
import { SubscriptionBadge } from './subscription-badge.js';
|
|
14
|
+
|
|
15
|
+
// Icons
|
|
16
|
+
const ChevronDownIcon = () => (
|
|
17
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
18
|
+
<polyline points="6,9 12,15 18,9"/>
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Sub-components
|
|
23
|
+
const UserMenuHeader: React.FC<{
|
|
24
|
+
user: UserMenuProps['user'];
|
|
25
|
+
subscription?: UserMenuProps['subscription'];
|
|
26
|
+
className?: string;
|
|
27
|
+
custom?: React.ReactNode;
|
|
28
|
+
}> = ({ user, subscription, className = '', custom }) => {
|
|
29
|
+
if (custom) {
|
|
30
|
+
return <div className={`user-menu__header ${className}`}>{custom}</div>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={`user-menu__header ${className}`}>
|
|
35
|
+
<div className="user-menu__header-avatar">
|
|
36
|
+
<UserAvatar user={user} size="lg" />
|
|
37
|
+
</div>
|
|
38
|
+
<div className="user-menu__header-info">
|
|
39
|
+
<div className="user-menu__header-name">
|
|
40
|
+
{user.fullName || user.firstName || 'User'}
|
|
41
|
+
</div>
|
|
42
|
+
{user.email && (
|
|
43
|
+
<div className="user-menu__header-email">
|
|
44
|
+
{user.email}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
{subscription && (
|
|
48
|
+
<div className="user-menu__header-subscription">
|
|
49
|
+
<SubscriptionBadge subscription={subscription} size="sm" />
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const UserMenuSection: React.FC<{
|
|
58
|
+
group: UserMenuGroup;
|
|
59
|
+
onItemClick?: (item: UserMenuItem) => void;
|
|
60
|
+
sectionIndex: number;
|
|
61
|
+
}> = ({ group, onItemClick }) => {
|
|
62
|
+
const handleItemClick = useCallback((item: UserMenuItem) => {
|
|
63
|
+
if (item.disabled) return;
|
|
64
|
+
|
|
65
|
+
// Handle navigation or action
|
|
66
|
+
if (item.href && !item.onClick) {
|
|
67
|
+
// Pure navigation - let default behavior handle it
|
|
68
|
+
window.location.href = item.href;
|
|
69
|
+
} else if (item.onClick) {
|
|
70
|
+
// Function call
|
|
71
|
+
item.onClick();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Call external handler
|
|
75
|
+
if (onItemClick) {
|
|
76
|
+
onItemClick(item);
|
|
77
|
+
}
|
|
78
|
+
}, [onItemClick]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={`user-menu__section ${group.divider ? 'user-menu__section--bordered' : ''}`}>
|
|
82
|
+
{group.title && (
|
|
83
|
+
<div className="user-menu__section-title">
|
|
84
|
+
{group.title}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<div className="user-menu__section-items">
|
|
88
|
+
{group.items.map((item) => {
|
|
89
|
+
const Element = item.href ? 'a' : 'button';
|
|
90
|
+
const elementProps = item.href ? { href: item.href } : { type: 'button' as const };
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Element
|
|
94
|
+
key={item.id}
|
|
95
|
+
onClick={() => handleItemClick(item)}
|
|
96
|
+
disabled={item.disabled}
|
|
97
|
+
className={`user-menu__item ${item.variant === 'danger' ? 'user-menu__item--danger' : ''} ${item.disabled ? 'user-menu__item--disabled' : ''}`}
|
|
98
|
+
data-testid={`user-menu-item-${item.id}`}
|
|
99
|
+
{...elementProps}
|
|
100
|
+
>
|
|
101
|
+
<div className="user-menu__item-content">
|
|
102
|
+
{item.icon && (
|
|
103
|
+
<span className="user-menu__item-icon">
|
|
104
|
+
{item.icon}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
<div className="user-menu__item-text">
|
|
108
|
+
<span className="user-menu__item-label">
|
|
109
|
+
{item.label}
|
|
110
|
+
</span>
|
|
111
|
+
{item.description && (
|
|
112
|
+
<span className="user-menu__item-description">
|
|
113
|
+
{item.description}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
{item.badge && (
|
|
119
|
+
<span className="user-menu__item-badge">
|
|
120
|
+
{item.badge}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
</Element>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const UserMenu: React.FC<UserMenuProps> = ({
|
|
132
|
+
user,
|
|
133
|
+
subscription,
|
|
134
|
+
trigger,
|
|
135
|
+
triggerProps,
|
|
136
|
+
groups,
|
|
137
|
+
showHeader = true,
|
|
138
|
+
headerCustom,
|
|
139
|
+
placement = 'bottom-right',
|
|
140
|
+
offset = 8,
|
|
141
|
+
width = 288,
|
|
142
|
+
maxHeight = 400,
|
|
143
|
+
onItemClick,
|
|
144
|
+
onSignOut,
|
|
145
|
+
open,
|
|
146
|
+
defaultOpen = false,
|
|
147
|
+
onOpenChange,
|
|
148
|
+
className = '',
|
|
149
|
+
headerClassName = '',
|
|
150
|
+
dropdownClassName = '',
|
|
151
|
+
'aria-label': ariaLabel = 'User menu',
|
|
152
|
+
'data-testid': testId = 'user-menu'
|
|
153
|
+
}) => {
|
|
154
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
155
|
+
// const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
156
|
+
|
|
157
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
158
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
159
|
+
|
|
160
|
+
// Use controlled or uncontrolled state
|
|
161
|
+
const isOpen = open !== undefined ? open : internalOpen;
|
|
162
|
+
|
|
163
|
+
const handleToggle = useCallback(() => {
|
|
164
|
+
const newOpen = !isOpen;
|
|
165
|
+
|
|
166
|
+
if (open === undefined) {
|
|
167
|
+
setInternalOpen(newOpen);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (onOpenChange) {
|
|
171
|
+
onOpenChange(newOpen);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reset focus when opening
|
|
175
|
+
// if (newOpen) {
|
|
176
|
+
// setFocusedIndex(-1);
|
|
177
|
+
// }
|
|
178
|
+
}, [isOpen, open, onOpenChange]);
|
|
179
|
+
|
|
180
|
+
const handleClose = useCallback(() => {
|
|
181
|
+
if (open === undefined) {
|
|
182
|
+
setInternalOpen(false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (onOpenChange) {
|
|
186
|
+
onOpenChange(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Return focus to trigger
|
|
190
|
+
if (triggerRef.current) {
|
|
191
|
+
triggerRef.current.focus();
|
|
192
|
+
}
|
|
193
|
+
}, [open, onOpenChange]);
|
|
194
|
+
|
|
195
|
+
const handleItemClick = useCallback((item: UserMenuItem) => {
|
|
196
|
+
// Handle sign out specially
|
|
197
|
+
if (item.variant === 'danger' && onSignOut) {
|
|
198
|
+
onSignOut();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Call external handler
|
|
202
|
+
if (onItemClick) {
|
|
203
|
+
onItemClick(item);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Close menu after item click
|
|
207
|
+
handleClose();
|
|
208
|
+
}, [onItemClick, onSignOut, handleClose]);
|
|
209
|
+
|
|
210
|
+
// Click outside handler
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
213
|
+
if (
|
|
214
|
+
isOpen &&
|
|
215
|
+
dropdownRef.current &&
|
|
216
|
+
!dropdownRef.current.contains(event.target as Node) &&
|
|
217
|
+
triggerRef.current &&
|
|
218
|
+
!triggerRef.current.contains(event.target as Node)
|
|
219
|
+
) {
|
|
220
|
+
handleClose();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (isOpen) {
|
|
225
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return () => {
|
|
229
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
230
|
+
};
|
|
231
|
+
}, [isOpen, handleClose]);
|
|
232
|
+
|
|
233
|
+
// Escape key handler
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
236
|
+
if (event.key === 'Escape' && isOpen) {
|
|
237
|
+
handleClose();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (isOpen) {
|
|
242
|
+
document.addEventListener('keydown', handleEscape);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return () => {
|
|
246
|
+
document.removeEventListener('keydown', handleEscape);
|
|
247
|
+
};
|
|
248
|
+
}, [isOpen, handleClose]);
|
|
249
|
+
|
|
250
|
+
// Get placement classes
|
|
251
|
+
const getPlacementClasses = () => {
|
|
252
|
+
switch (placement) {
|
|
253
|
+
case 'bottom-left':
|
|
254
|
+
return 'user-menu__dropdown--bottom-left';
|
|
255
|
+
case 'bottom-right':
|
|
256
|
+
return 'user-menu__dropdown--bottom-right';
|
|
257
|
+
case 'top-left':
|
|
258
|
+
return 'user-menu__dropdown--top-left';
|
|
259
|
+
case 'top-right':
|
|
260
|
+
return 'user-menu__dropdown--top-right';
|
|
261
|
+
default:
|
|
262
|
+
return 'user-menu__dropdown--bottom-right';
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Default trigger
|
|
267
|
+
const defaultTrigger = (
|
|
268
|
+
<Button
|
|
269
|
+
ref={triggerRef}
|
|
270
|
+
variant="ghost"
|
|
271
|
+
className="user-menu__trigger-default"
|
|
272
|
+
{...triggerProps}
|
|
273
|
+
>
|
|
274
|
+
<UserAvatar user={user} size="sm" />
|
|
275
|
+
<span className="user-menu__trigger-name">
|
|
276
|
+
{user.firstName || user.fullName || 'Account'}
|
|
277
|
+
</span>
|
|
278
|
+
{subscription && (
|
|
279
|
+
<SubscriptionBadge subscription={subscription} size="sm" />
|
|
280
|
+
)}
|
|
281
|
+
<ChevronDownIcon />
|
|
282
|
+
</Button>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Custom trigger with added props
|
|
286
|
+
const customTrigger = trigger ? (
|
|
287
|
+
React.cloneElement(trigger as React.ReactElement, {
|
|
288
|
+
ref: triggerRef,
|
|
289
|
+
onClick: handleToggle,
|
|
290
|
+
'aria-expanded': isOpen,
|
|
291
|
+
'aria-haspopup': 'true',
|
|
292
|
+
'aria-label': ariaLabel,
|
|
293
|
+
className: `${(trigger as React.ReactElement).props.className || ''} user-menu__trigger`.trim()
|
|
294
|
+
})
|
|
295
|
+
) : null;
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className={`user-menu ${className}`} data-testid={testId}>
|
|
299
|
+
{/* Trigger */}
|
|
300
|
+
{customTrigger || (
|
|
301
|
+
<button
|
|
302
|
+
ref={triggerRef}
|
|
303
|
+
onClick={handleToggle}
|
|
304
|
+
className="user-menu__trigger"
|
|
305
|
+
aria-expanded={isOpen}
|
|
306
|
+
aria-haspopup="true"
|
|
307
|
+
aria-label={ariaLabel}
|
|
308
|
+
>
|
|
309
|
+
{defaultTrigger}
|
|
310
|
+
</button>
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{/* Dropdown */}
|
|
314
|
+
{isOpen && (
|
|
315
|
+
<div
|
|
316
|
+
ref={dropdownRef}
|
|
317
|
+
className={`user-menu__dropdown ${getPlacementClasses()} ${dropdownClassName}`}
|
|
318
|
+
role="menu"
|
|
319
|
+
aria-label={ariaLabel}
|
|
320
|
+
style={{
|
|
321
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
322
|
+
maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight,
|
|
323
|
+
marginTop: placement.includes('bottom') ? `${offset}px` : undefined,
|
|
324
|
+
marginBottom: placement.includes('top') ? `${offset}px` : undefined
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{/* User Header */}
|
|
328
|
+
{showHeader && (
|
|
329
|
+
<UserMenuHeader
|
|
330
|
+
user={user}
|
|
331
|
+
subscription={subscription}
|
|
332
|
+
className={headerClassName}
|
|
333
|
+
custom={headerCustom}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* Menu Sections */}
|
|
338
|
+
<div className="user-menu__sections">
|
|
339
|
+
{groups.map((group, sectionIndex) => (
|
|
340
|
+
<UserMenuSection
|
|
341
|
+
key={group.id}
|
|
342
|
+
group={group}
|
|
343
|
+
onItemClick={handleItemClick}
|
|
344
|
+
sectionIndex={sectionIndex}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export default UserMenu;
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Production-ready design system package with stan-design theme
|
|
4
4
|
*
|
|
5
5
|
* Auto-generated exports for:
|
|
6
|
-
* -
|
|
6
|
+
* - 89 UI components
|
|
7
7
|
* - 67 custom hooks
|
|
8
8
|
* - 6 utility functions
|
|
9
9
|
* - 29 theme system components
|
|
@@ -59,7 +59,11 @@ export { Menu } from './components/ui/navigation/menu.js';
|
|
|
59
59
|
export { Pagination } from './components/ui/navigation/pagination.js';
|
|
60
60
|
export { Sidebar } from './components/ui/navigation/sidebar.js';
|
|
61
61
|
export { Stepper } from './components/ui/navigation/stepper.js';
|
|
62
|
+
export { SubscriptionBadge } from './components/ui/navigation/subscription-badge.js';
|
|
62
63
|
export { Tabs } from './components/ui/navigation/tabs.js';
|
|
64
|
+
export { UserAvatar } from './components/ui/navigation/user-avatar.js';
|
|
65
|
+
export { UserMenuExamples } from './components/ui/navigation/user-menu-examples.js';
|
|
66
|
+
export { UserMenu } from './components/ui/navigation/user-menu.js';
|
|
63
67
|
export { Chart } from './components/ui/data-display/chart.js';
|
|
64
68
|
export { DataGridSimple } from './components/ui/data-display/data-grid-simple.js';
|
|
65
69
|
export { DataGrid } from './components/ui/data-display/data-grid.js';
|