| 1 | // The three settings popovers (theme, model, API key) all behave the same way: |
| 2 | // a chip toggles a panel, clicking outside or pressing Escape closes it, and |
| 3 | // opening one closes the others. That last part matters now that the model and |
| 4 | // key panels are anchored side by side in the chat header — they occupy the |
| 5 | // same strip, so two open at once would stack. |
| 6 | // |
| 7 | // Registered panels are tracked here rather than broadcast on the DOM, since |
| 8 | // they're all bound at startup and never move. |
| 9 | const panels = []; |
| 10 | |
| 11 | /** |
| 12 | * Wire a chip to its panel. `onOpen` runs after the panel is shown — use it to |
| 13 | * seed fields or move focus. |
| 14 | */ |
| 15 | export function bindPanel(button, panel, onOpen) { |
| 16 | const toggle = (open) => { |
| 17 | panel.hidden = !open; |
| 18 | button.setAttribute('aria-expanded', String(open)); |
| 19 | if (open) onOpen?.(); |
| 20 | }; |
| 21 | panels.push(toggle); |
| 22 | |
| 23 | button.addEventListener('click', (e) => { |
| 24 | e.stopPropagation(); |
| 25 | if (button.disabled) return; |
| 26 | const open = panel.hidden; |
| 27 | closeAll(); |
| 28 | toggle(open); |
| 29 | }); |
| 30 | // Clicks inside a panel are for the controls in it, not a signal to dismiss. |
| 31 | panel.addEventListener('click', (e) => e.stopPropagation()); |
| 32 | } |
| 33 | |
| 34 | export function closeAll() { |
| 35 | for (const toggle of panels) toggle(false); |
| 36 | } |
| 37 | |
| 38 | document.addEventListener('click', closeAll); |
| 39 | document.addEventListener('keydown', (e) => { |
| 40 | if (e.key === 'Escape') closeAll(); |
| 41 | }); |