Files
odysseus/static/js/settings/shell.js
T
Léo 1c5f60539f refactor(settings): move the shell out of settings.js into settings/
settings.js is 5,721 lines and the registry/navigation/search/sidebar/
lifecycle primitives already live in static/js/settings/. What was left
behind in the coordinator was the layer above them: what happens when a
panel becomes active, where an admin-managed tab is handed to admin.js,
which elements are admin-only, the Appearance window fade, and the
public open/close. That layer reached module-global `modalEl` and
`initialized` directly, so none of it could be exercised without
booting every panel in the file — and every panel I eventually move out
would have to route back through it.

Three modules, no behavior change:

  shell.js       panel-activation side effects, the admin handoff,
                 .admin-only visibility, open()/close(). Takes what it
                 needs from settings.js as injected callbacks, the same
                 shape bindSettingsNavigation() already uses, so it
                 holds no panel state.
  peek.js        the Appearance window fade and its toggle. It is window
                 chrome rather than Appearance panel data, and it has to
                 be cleared when the user leaves that panel.
  oauthReturn.js the once-per-load return path from the Google OAuth
                 redirect. It was an IIFE running at module evaluation
                 in the middle of a 5,700-line file.

settings.js keeps open/close/syncAdminVisibility as exports, so every
caller (app.js, calendar.js, chatStream.js, gallery.js, admin.js,
modelPicker.js, slashCommands.js, chatRenderer.js, emailLibrary.js) is
untouched. 5,721 -> 5,583 lines; the settings/ modules go 887 -> 1,107.

The real-ESM coordinator smoke now links the three new files and asserts
what moved: admin-only elements hidden for a non-admin and shown for an
admin, an admin-managed tab click handed to admin.js without a second
local activation, and the Peek fade applying on Appearance and clearing
when the user navigates away. The OAuth test follows its handler to the
new file and additionally pins the coordinator wiring, since "uses the
module-local open()" is now a property of the seam rather than of one
source slice.

No new module needs a cache-busting query or an sw.js precache entry:
the existing settings/ submodules have neither, they load transitively
from settings.js's versioned URL, and sw.js serves JS network-first.

admin.js stays where it is. It has no shell to extract — open() and
close() already delegate to settingsModule, and its 4,122 lines are all
panel code. That is a panel split, not this one.
2026-09-30 09:45:23 +02:00

106 lines
3.9 KiB
JavaScript

// Settings shell coordination.
//
// settings.js still owns every panel's data loading. This module owns the shell
// around them: what happens when a panel becomes active, where an
// admin-managed tab is handed off to admin.js, which elements are admin-only,
// and the public open/close entry points.
//
// It keeps no panel state. Everything it needs from the panels arrives through
// the options below, so panels can move out of settings.js one at a time
// without this file changing — and so the shell's behavior is testable without
// booting a panel.
import { activateSettingsPanel, getActiveSettingsTab } from './navigation.js';
import { showSettingsModal, hideSettingsModal } from './lifecycle.js';
import { isAdminManagedSettingsTab } from './registry.js';
import { syncSettingsPeek } from './peek.js';
/**
* @param {object} options
* @param {() => Element|null} options.getModal the Settings modal, once initialized
* @param {() => void} options.ensureInitialized first-open panel initialization
* @param {() => void} options.syncAppearanceCheckboxes Appearance panel state refresh
* @param {() => void} options.refreshAiModelEndpoints AI panel endpoint refresh
* @param {() => boolean} options.isAdmin current admin status
* @param {() => object|null} options.getAdminModule the lazily loaded admin module
*/
export function createSettingsShell(options = {}) {
const getModal = options.getModal;
const ensureInitialized = options.ensureInitialized;
const syncAppearanceCheckboxes = options.syncAppearanceCheckboxes;
const refreshAiModelEndpoints = options.refreshAiModelEndpoints;
const isAdmin = options.isAdmin;
const getAdminModule = options.getAdminModule;
const _modal = () => (typeof getModal === 'function' ? getModal() : null);
const _admin = () => (typeof getAdminModule === 'function' ? getAdminModule() : null);
function onPanelActivated(tab) {
// Appearance keeps its existing transparent preview behavior.
document.body.classList.toggle('settings-appearance-open', tab === 'appearance');
syncSettingsPeek(_modal(), tab === 'appearance');
// AI endpoints are intentionally refreshed only when entering the AI panel.
if (tab === 'ai' && typeof refreshAiModelEndpoints === 'function') {
refreshAiModelEndpoints();
}
}
function openAdminTab(tab) {
const admin = _admin();
if (admin && typeof admin.open === 'function') {
admin.open(tab);
return true;
}
return false;
}
function syncAdminVisibility() {
const modalEl = _modal();
if (!modalEl) return;
const admin = typeof isAdmin === 'function' ? !!isAdmin() : false;
modalEl.querySelectorAll('.admin-only').forEach(el => {
el.style.display = admin ? '' : 'none';
});
}
function open(tab) {
if (typeof ensureInitialized === 'function') ensureInitialized();
if (typeof syncAppearanceCheckboxes === 'function') syncAppearanceCheckboxes();
const modalEl = _modal();
showSettingsModal(modalEl);
syncAdminVisibility();
if (tab) {
activateSettingsPanel(modalEl, tab);
}
// Preserve existing panel-specific side effects when Settings is opened
// directly to a tab as well as when the user navigates there.
const activeTab = tab || getActiveSettingsTab(modalEl);
onPanelActivated(activeTab);
// Auto-init admin data if showing an admin tab.
const admin = _admin();
if (isAdminManagedSettingsTab(activeTab) && admin && !admin._initialized) {
admin._initData();
}
}
function close() {
const modalEl = _modal();
if (!modalEl) return;
// Always clear the Appearance state so the rest of the app does not remain
// dimmed if Settings is closed while that panel is active.
document.body.classList.remove('settings-appearance-open');
syncSettingsPeek(modalEl, false);
hideSettingsModal(modalEl);
}
return { open, close, onPanelActivated, openAdminTab, syncAdminVisibility };
}