// Who's-here list (SOCIAL_RESET brief, Feature 4). Hold Tab: a translucent list // of the DJs in the booth (you first), each with their avatar hue as a swatch. // Pure view — the data comes from NetClient's roster; no network of its own. import { bodyCss, type AvatarLook } from '../net/avatarLook'; export interface RosterEntry { id: number; name: string; look: AvatarLook; } export class Roster { private readonly el: HTMLDivElement; private peers: RosterEntry[] = []; private self: { name: string; look: AvatarLook }; private shown = false; constructor(self: { name: string; look: AvatarLook }) { this.self = self; this.injectStyle(); this.el = document.createElement('div'); this.el.className = 'tcr-panel hidden'; document.body.appendChild(this.el); window.addEventListener('keydown', this.onDown); window.addEventListener('keyup', this.onUp); // Tab can steal focus / fire blur — hide defensively. window.addEventListener('blur', () => this.show(false)); } /** Local player's name/look (the mirror can change live). */ setSelf(self: { name: string; look: AvatarLook }): void { this.self = self; if (this.shown) this.render(); } setPeers(peers: RosterEntry[]): void { this.peers = peers; if (this.shown) this.render(); } private onDown = (e: KeyboardEvent): void => { if (e.code !== 'Tab') return; e.preventDefault(); // don't tab-focus the page furniture if (!e.repeat) this.show(true); }; private onUp = (e: KeyboardEvent): void => { if (e.code !== 'Tab') return; e.preventDefault(); this.show(false); }; private show(v: boolean): void { if (v === this.shown) return; this.shown = v; if (v) this.render(); this.el.classList.toggle('hidden', !v); } private render(): void { const rows: string[] = []; const row = (name: string, css: string, you: boolean) => `