// Settings reuses the account, appearance, voice and memory owners. // Moving nodes preserves state and handlers; this module never writes profiles or roles. (() => { 'use strict'; const $ = id => document.getElementById(id); const sections = new Set(['account', 'appearance', 'preferences', 'privacy', 'security', 'admin']); let initialized = false, dialog, current = 'account', opener = null, generation = 0; let memory = null, statusHome = null; const panel = section => dialog?.querySelector(`[data-settings-panel="${section}"]`); const text = (id, value) => { if ($(id)) $(id).textContent = value; }; function restoreStatus() { if (statusHome && $('featureStatus')) { const {parent, next} = statusHome; parent.insertBefore($('featureStatus'), next?.parentNode === parent ? next : null); statusHome = null; } } function updateMemoryStatus() { text('settingsMemoryStatus', !memory || memory.hidden ? 'Memory controls are unavailable for this account. No memory has been changed.' : 'Review or edit your saved memory below. Clearing memory does not delete conversations.'); } function identity(user, changed) { text('settingsAccountName', user?.display_name || user?.username || ''); text('settingsAccountUsername', user ? `@${user.username}` : ''); text('settingsAccountRole', user ? (user.role === 'admin' ? 'Administrator' : 'User') : ''); dialog.querySelectorAll('[data-settings-target="admin"], [data-settings-open="admin"]').forEach(button => { button.hidden = user?.role !== 'admin'; }); if (!user || changed) close(false); else if (current === 'admin' && user.role !== 'admin') select('account'); updateMemoryStatus(); } async function loadAppearance(version) { const message = $('preferencesMessage'); if (message) message.textContent = 'Loading your saved appearance…'; try { if (window.HopeAppearance?.load) await window.HopeAppearance.load(); else throw new Error('Appearance settings are unavailable. Reload Hope to try again.'); } catch (error) { if (version === generation && window.HopeAuth?.user && message) message.textContent = error.message; } } function select(section) { if (!sections.has(section)) section = 'account'; if (section === 'admin' && window.HopeAuth?.user?.role !== 'admin') section = 'account'; current = section; const version = ++generation; restoreStatus(); dialog.querySelectorAll('[data-settings-panel]').forEach(item => { item.hidden = item.dataset.settingsPanel !== section; }); dialog.querySelectorAll('[data-settings-target]').forEach(button => { const active = button.dataset.settingsTarget === section; button.classList.toggle('selected', active); if (active) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current'); }); // auth.js keeps its polling and provisioning handlers; never copy them. if ($('adminSection')) $('adminSection').hidden = section !== 'admin'; $('adminBtn')?.setAttribute('aria-expanded', String(section === 'admin')); if (section === 'admin') $('refreshUsers')?.click(); if (section === 'appearance') loadAppearance(version); if (section === 'preferences') { if (window.HopeVoiceSettings?.load) window.HopeVoiceSettings.load(); else { if (typeof renderVoiceList === 'function') renderVoiceList(); if (typeof loadVoiceCatalog === 'function') loadVoiceCatalog(); if (typeof loadModelSettings === 'function') loadModelSettings(); } } if (section === 'privacy') { if (!memory) memory = window.HopeFeatures?.mountMemory?.($('privacyMemory') || panel('privacy')) || null; const status = $('featureStatus'); if (status && status.parentNode !== panel('privacy')) { statusHome = {parent: status.parentNode, next: status.nextSibling}; panel('privacy').append(status); } updateMemoryStatus(); } } function open(section = 'account') { initialize(); if (!dialog || !window.HopeAuth?.user) return; if (!dialog.open) opener = document.activeElement; if ($('chatSidebar')?.classList.contains('drawer-open')) $('drawerClose')?.click(); select(section); if (!dialog.open) dialog.showModal(); $('settingsClose')?.focus(); } function close(restoreFocus = true) { generation++; restoreStatus(); if (!dialog?.open) return; dialog.close(); if ($('adminSection')) $('adminSection').hidden = true; $('adminBtn')?.setAttribute('aria-expanded', 'false'); if (restoreFocus) { const target = [opener, $('settingsBtn'), $('drawerOpen')].find(item => item?.isConnected && !item.closest('[hidden], [inert]') && item.getClientRects().length); target?.focus(); } } function initialize() { if (initialized) return; dialog = $('settingsDialog'); if (!dialog || !window.HopeAuth) return; initialized = true; const sheet = $('settingsSheet'); if (sheet && panel('preferences')) { panel('preferences').append(sheet); sheet.hidden = false; sheet.inert = false; sheet.classList.remove('open'); sheet.removeAttribute('role'); sheet.removeAttribute('aria-modal'); } if ($('preferencesForm') && panel('appearance')) panel('appearance').append($('preferencesForm')); if ($('adminSection') && panel('admin')) panel('admin').append($('adminSection')); for (const id of ['closeSheetBtn', 'closePreferences']) { if ($(id)) $(id).addEventListener('click', () => close(), true); } $('settingsClose')?.addEventListener('click', () => close()); dialog.addEventListener('cancel', event => { event.preventDefault(); close(); }); dialog.addEventListener('keydown', event => { if (event.key !== 'Tab' || $('accountDialog')?.open) return; const controls = [...dialog.querySelectorAll('button, input, select, textarea, a[href], [tabindex]')].filter(item => !item.disabled && item.tabIndex >= 0 && !item.closest('[hidden], [inert]') && item.getClientRects().length); const first = controls[0], last = controls[controls.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } }); dialog.addEventListener('close', () => { // Native close events are queued; a later open must not be dismantled. if (dialog.open) return; restoreStatus(); if ($('adminSection')) $('adminSection').hidden = true; $('adminBtn')?.setAttribute('aria-expanded', 'false'); }); dialog.addEventListener('click', event => { const button = event.target.closest('[data-settings-target]'); if (!button || !dialog.contains(button)) return; event.preventDefault(); event.stopPropagation(); select(button.dataset.settingsTarget); }, true); document.addEventListener('click', event => { const button = event.target.closest('[data-settings-open], #adminBtn, #openPreferences'); if (!button || dialog.contains(button)) return; event.preventDefault(); event.stopImmediatePropagation(); open(button.dataset.settingsOpen || (button.id === 'adminBtn' ? 'admin' : 'appearance')); }, true); dialog.querySelectorAll('[data-settings-signout]').forEach(button => { button.addEventListener('click', () => { close(false); $('logoutBtn')?.click(); }); }); memory = window.HopeFeatures?.mountMemory?.($('privacyMemory') || panel('privacy')) || null; if (memory) new MutationObserver(updateMemoryStatus).observe(memory, {attributes: true, attributeFilter: ['hidden']}); window.HopeAuth.onChange(identity); } window.HopeSettings = {open, close, initialize, get section() {return current;}, get isOpen() {return !!dialog?.open;}}; initialize(); })();