File size: 5,382 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
 * AccountMenu β€” enterprise-grade account popover anchored to the sidebar avatar.
 *
 * Structure (Claude / Slack / GitHub pattern):
 *   [Avatar]  Display Name
 *             email / username
 *   ─────────────────────────
 *   Personalization
 *   Settings
 *   ─────────────────────────
 *   About
 *   ─────────────────────────
 *   Log out
 *
 * UX rules:
 *  - Opens upward from the avatar (bottom-left anchor)
 *  - Closes on outside click or Esc
 *  - Keyboard-navigable (arrow keys, Enter)
 *  - Log out: one click, no confirmation, instant redirect
 *  - Identity at top, exit at bottom
 */
import React, { useEffect, useRef, useCallback } from 'react';
import { Settings, LogOut, Palette, Info } from 'lucide-react';
import UserAvatar from './UserAvatar';
export default function AccountMenu({ user, onClose, onOpenSettings, onOpenProfile, onOpenAbout, onLogout, }) {
    const menuRef = useRef(null);
    const itemsRef = useRef([]);
    // Close on outside click
    useEffect(() => {
        function handleClickOutside(e) {
            if (menuRef.current && !menuRef.current.contains(e.target)) {
                onClose();
            }
        }
        // Delay to avoid the same click that opened the menu from closing it
        const timer = setTimeout(() => {
            document.addEventListener('mousedown', handleClickOutside);
        }, 0);
        return () => {
            clearTimeout(timer);
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [onClose]);
    // Keyboard navigation
    const handleKeyDown = useCallback((e) => {
        const items = itemsRef.current.filter(Boolean);
        const currentIndex = items.indexOf(document.activeElement);
        switch (e.key) {
            case 'Escape':
                e.preventDefault();
                onClose();
                break;
            case 'ArrowDown':
                e.preventDefault();
                if (currentIndex < items.length - 1) {
                    items[currentIndex + 1]?.focus();
                }
                else {
                    items[0]?.focus();
                }
                break;
            case 'ArrowUp':
                e.preventDefault();
                if (currentIndex > 0) {
                    items[currentIndex - 1]?.focus();
                }
                else {
                    items[items.length - 1]?.focus();
                }
                break;
            case 'Tab':
                e.preventDefault();
                onClose();
                break;
        }
    }, [onClose]);
    // Focus first item on mount
    useEffect(() => {
        const first = itemsRef.current.find(Boolean);
        first?.focus();
    }, []);
    const setItemRef = (index) => (el) => {
        if (el)
            itemsRef.current[index] = el;
    };
    const menuItemClass = [
        'w-full text-left px-3 py-2 text-[13px] rounded-lg',
        'flex items-center gap-2.5',
        'text-white/80 hover:bg-white/8 hover:text-white',
        'focus:bg-white/8 focus:text-white focus:outline-none',
        'transition-colors cursor-pointer',
    ].join(' ');
    return (<div ref={menuRef} role="menu" aria-label="Account menu" onKeyDown={handleKeyDown} className="absolute bottom-[72px] left-3 w-[260px] bg-[#0f0f1a] border border-white/10 rounded-2xl shadow-2xl z-50 ring-1 ring-white/5 overflow-hidden" style={{ animation: 'accountMenuIn 150ms ease-out' }}>
      {/* Identity header */}
      <div className="px-4 py-3.5 flex items-center gap-3 border-b border-white/5">
        <UserAvatar displayName={user.display_name || user.username} avatarUrl={user.avatar_url} size={36}/>
        <div className="min-w-0 flex-1">
          <div className="text-sm font-semibold text-white truncate">
            {user.display_name || user.username}
          </div>
          <div className="text-[11px] text-white/40 truncate">
            {user.email || `@${user.username}`}
          </div>
        </div>
      </div>

      {/* Main actions */}
      <div className="px-2 py-1.5">
        <button ref={setItemRef(0)} role="menuitem" className={menuItemClass} onClick={() => { onClose(); onOpenProfile(); }}>
          <Palette size={15} className="text-white/40 shrink-0"/>
          Personalization
        </button>
        <button ref={setItemRef(1)} role="menuitem" className={menuItemClass} onClick={() => { onClose(); onOpenSettings(); }}>
          <Settings size={15} className="text-white/40 shrink-0"/>
          Settings
        </button>
      </div>

      {/* About */}
      <div className="border-t border-white/5 px-2 py-1.5">
        <button ref={setItemRef(2)} role="menuitem" className={menuItemClass} onClick={() => { onClose(); onOpenAbout(); }}>
          <Info size={15} className="text-white/40 shrink-0"/>
          About
        </button>
      </div>

      {/* Log out β€” always last, visually separated */}
      <div className="border-t border-white/5 px-2 py-1.5">
        <button ref={setItemRef(3)} role="menuitem" className={menuItemClass} onClick={onLogout}>
          <LogOut size={15} className="text-white/40 shrink-0"/>
          Log out
        </button>
      </div>
    </div>);
}