Changeset 0.25.0 (#160)

This commit is contained in:
2026-03-08 16:54:17 +00:00
parent 937be26578
commit 2b01d540d6
63 changed files with 6942 additions and 2773 deletions

154
src/js/user-menu.js Normal file
View File

@@ -0,0 +1,154 @@
// ==========================================
// 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 = {
primary: null,
_instances: new Map(),
create(opts) {
const pfx = opts.id || '';
const instance = {
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,
_listeners: [],
// ── 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(); });
},
// ── Lifecycle ───────────────────────
destroy() {
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
this._listeners = [];
UserMenu._instances.delete(this.id);
if (UserMenu.primary === this) UserMenu.primary = null;
},
// ── Internal ────────────────────────
_on(el, event, handler) {
if (!el) return;
el.addEventListener(event, handler);
this._listeners.push({ el, event, handler });
},
};
UserMenu._instances.set(pfx, instance);
return instance;
},
get(id) { return this._instances.get(id) || null; },
};