File size: 5,734 Bytes
23b413b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
 * 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 interface AccountMenuUser {
  id: string
  username: string
  display_name: string
  email: string
  avatar_url: string
}

interface AccountMenuProps {
  user: AccountMenuUser
  onClose: () => void
  onOpenSettings: () => void
  onOpenProfile: () => void
  onOpenAbout: () => void
  onLogout: () => void
}

export default function AccountMenu({
  user,
  onClose,
  onOpenSettings,
  onOpenProfile,
  onOpenAbout,
  onLogout,
}: AccountMenuProps) {
  const menuRef = useRef<HTMLDivElement>(null)
  const itemsRef = useRef<HTMLButtonElement[]>([])

  // Close on outside click
  useEffect(() => {
    function handleClickOutside(e: MouseEvent) {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
        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: React.KeyboardEvent) => {
    const items = itemsRef.current.filter(Boolean)
    const currentIndex = items.indexOf(document.activeElement as HTMLButtonElement)

    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: number) => (el: HTMLButtonElement | null) => {
    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>
  )
}