// ========================================== // Chat Switchboard — UserMenu Component // ========================================== // v0.25.0: Extracted from chat.html + ui-core.js. // Reusable across all authenticated surfaces. // Pattern: Go template partial (user-menu.html) + JS factory (this file). // // Usage: // const menu = UserMenu.create({ id: 'global' }); // menu.setUser({ display_name: 'Jeff', username: 'jeff', avatar: null }); // menu.showAdmin(true); // menu.destroy(); const UserMenu = { ...createComponentRegistry('UserMenu'), create(opts) { const pfx = opts.id || ''; const instance = componentMixin({ id: pfx, btnEl: document.getElementById(pfx + 'userMenuBtn'), avatarEl: document.getElementById(pfx + 'userAvatar'), letterEl: document.getElementById(pfx + 'avatarLetter'), nameEl: document.getElementById(pfx + 'userName'), flyoutEl: document.getElementById(pfx + 'userFlyout'), settingsEl: document.getElementById(pfx + 'menuSettings'), adminEl: document.getElementById(pfx + 'menuAdmin'), teamAdminEl: document.getElementById(pfx + 'menuTeamAdmin'), debugEl: document.getElementById(pfx + 'menuDebug'), signoutEl: document.getElementById(pfx + 'menuSignout'), _open: false, // ── User display ──────────────────── setUser(user) { if (!user) return; const name = user.display_name || user.username || 'User'; const letter = (name[0] || '?').toUpperCase(); // Avatar image or initial letter if (this.avatarEl) { let img = this.avatarEl.querySelector('.user-avatar-img'); if (user.avatar) { if (this.letterEl) this.letterEl.style.display = 'none'; if (img) { img.src = user.avatar; } else { img = document.createElement('img'); img.src = user.avatar; img.alt = ''; img.className = 'user-avatar-img'; img.onerror = function() { this.remove(); if (instance.letterEl) { instance.letterEl.style.display = ''; instance.letterEl.textContent = letter; } }; this.avatarEl.insertBefore(img, this.letterEl); } } else { if (img) img.remove(); if (this.letterEl) { this.letterEl.style.display = ''; this.letterEl.textContent = letter; } } } if (this.nameEl) this.nameEl.textContent = name; }, // ── Visibility controls ───────────── showAdmin(show) { if (this.adminEl) this.adminEl.style.display = show ? '' : 'none'; if (this.debugEl) this.debugEl.style.display = show ? '' : 'none'; }, showTeamAdmin(show) { if (this.teamAdminEl) this.teamAdminEl.style.display = show ? '' : 'none'; }, // ── Flyout toggle ─────────────────── open() { if (this.flyoutEl) this.flyoutEl.classList.add('open'); this._open = true; }, close() { if (this.flyoutEl) this.flyoutEl.classList.remove('open'); this._open = false; }, toggle() { if (this._open) this.close(); else this.open(); }, // ── Event wiring ──────────────────── /** * Wire event handlers. Accepts callbacks for menu actions. * Call once after create(). */ bind(handlers) { handlers = handlers || {}; // Toggle flyout on avatar click this._on(this.btnEl, 'click', (e) => { e.stopPropagation(); this.toggle(); }); // Close on outside click this._on(document, 'click', (e) => { if (this._open && !e.target.closest('#' + pfx + 'userMenuWrap')) { this.close(); } }); // Menu items this._on(this.settingsEl, 'click', () => { this.close(); if (handlers.onSettings) handlers.onSettings(); }); this._on(this.adminEl, 'click', () => { this.close(); if (handlers.onAdmin) handlers.onAdmin(); }); this._on(this.teamAdminEl, 'click', () => { this.close(); if (handlers.onTeamAdmin) handlers.onTeamAdmin(); }); this._on(this.debugEl, 'click', () => { this.close(); if (handlers.onDebug) handlers.onDebug(); }); this._on(this.signoutEl, 'click', () => { this.close(); if (handlers.onSignout) handlers.onSignout(); }); }, }, UserMenu); UserMenu._register(pfx, instance); return instance; }, };