191 lines
6.7 KiB
JavaScript
Executable File
191 lines
6.7 KiB
JavaScript
Executable File
document.documentElement.classList.add('js');
|
|
|
|
function readThemePreference(key, fallback) {
|
|
try {
|
|
return localStorage.getItem(key) || fallback;
|
|
} catch (error) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
const moduleName = document.documentElement.dataset.module || '';
|
|
const mainThemeMode = readThemePreference('nexus.theme', 'day');
|
|
const mainThemeAccent = readThemePreference('nexus.accent', 'logo');
|
|
const moduleThemeMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : mainThemeMode;
|
|
const moduleThemeAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : mainThemeAccent;
|
|
|
|
function normalizeThemeValue(value, allowed, fallback) {
|
|
return allowed.includes(value) ? value : fallback;
|
|
}
|
|
|
|
function storedTheme() {
|
|
const rawMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : readThemePreference('nexus.theme', 'day');
|
|
const rawAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : readThemePreference('nexus.accent', 'logo');
|
|
const mode = moduleName ? normalizeThemeValue(rawMode, ['inherit', 'day', 'night'], 'inherit') : normalizeThemeValue(rawMode, ['day', 'night'], 'day');
|
|
const accent = moduleName ? normalizeThemeValue(rawAccent, ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'], 'inherit') : normalizeThemeValue(rawAccent, ['logo', 'pink', 'cyan', 'orange', 'green'], 'logo');
|
|
return { mode, accent };
|
|
}
|
|
|
|
function applyTheme(mode, accent, persist = true) {
|
|
const allowedModes = moduleName ? ['inherit', 'day', 'night'] : ['day', 'night'];
|
|
const allowedAccents = moduleName ? ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'] : ['logo', 'pink', 'cyan', 'orange', 'green'];
|
|
const normalizedMode = normalizeThemeValue(mode, allowedModes, moduleName ? 'inherit' : 'day');
|
|
const normalizedAccent = normalizeThemeValue(accent, allowedAccents, moduleName ? 'inherit' : 'logo');
|
|
const effectiveMode = normalizedMode === 'inherit' ? mainThemeMode : normalizedMode;
|
|
const effectiveAccent = normalizedAccent === 'inherit' ? mainThemeAccent : normalizedAccent;
|
|
document.documentElement.dataset.theme = effectiveMode;
|
|
document.documentElement.dataset.accent = effectiveAccent;
|
|
try {
|
|
if (persist) {
|
|
if (moduleName) {
|
|
if (normalizedMode === 'inherit') {
|
|
localStorage.removeItem(`nexus.module.${moduleName}.theme`);
|
|
} else {
|
|
localStorage.setItem(`nexus.module.${moduleName}.theme`, normalizedMode);
|
|
}
|
|
if (normalizedAccent === 'inherit') {
|
|
localStorage.removeItem(`nexus.module.${moduleName}.accent`);
|
|
} else {
|
|
localStorage.setItem(`nexus.module.${moduleName}.accent`, normalizedAccent);
|
|
}
|
|
} else {
|
|
localStorage.setItem('nexus.theme', normalizedMode);
|
|
localStorage.setItem('nexus.accent', normalizedAccent);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore blocked storage; the current page still receives the theme.
|
|
}
|
|
return { mode: normalizedMode, accent: normalizedAccent };
|
|
}
|
|
|
|
const initialTheme = applyTheme(moduleThemeMode, moduleThemeAccent, false);
|
|
|
|
const themeModeSelect = document.querySelector('[data-theme-mode]');
|
|
const themeAccentSelect = document.querySelector('[data-theme-accent]');
|
|
|
|
if (themeModeSelect) {
|
|
themeModeSelect.value = initialTheme.mode;
|
|
themeModeSelect.addEventListener('change', () => {
|
|
const current = storedTheme();
|
|
applyTheme(themeModeSelect.value, current.accent);
|
|
});
|
|
}
|
|
|
|
if (themeAccentSelect) {
|
|
themeAccentSelect.value = initialTheme.accent;
|
|
themeAccentSelect.addEventListener('change', () => {
|
|
const current = storedTheme();
|
|
applyTheme(current.mode, themeAccentSelect.value);
|
|
});
|
|
}
|
|
|
|
for (const element of document.querySelectorAll('[data-reveal]')) {
|
|
element.classList.add('reveal');
|
|
}
|
|
|
|
const setupTabs = document.querySelectorAll('[data-setup-tab-target]');
|
|
if (setupTabs.length > 0) {
|
|
const setupPanels = document.querySelectorAll('.setup-db-panel');
|
|
const setupControls = document.querySelectorAll('.setup-db-panel input, .setup-db-panel select, .setup-db-panel textarea');
|
|
for (const control of setupControls) {
|
|
if (control.required) {
|
|
control.dataset.setupRequired = 'true';
|
|
}
|
|
}
|
|
|
|
const activateSetupTab = (targetId) => {
|
|
for (const tab of setupTabs) {
|
|
const isActive = tab.dataset.setupTabTarget === targetId;
|
|
tab.classList.toggle('is-active', isActive);
|
|
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
}
|
|
for (const panel of setupPanels) {
|
|
const isActive = panel.id === targetId;
|
|
panel.hidden = !isActive;
|
|
for (const control of panel.querySelectorAll('input, select, textarea')) {
|
|
if (control.dataset.setupRequired === 'true') {
|
|
control.required = isActive;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const tab of setupTabs) {
|
|
tab.addEventListener('click', () => {
|
|
activateSetupTab(tab.dataset.setupTabTarget);
|
|
});
|
|
}
|
|
|
|
const activeSetupTab = document.querySelector('[data-setup-tab-target].is-active') || setupTabs[0];
|
|
activateSetupTab(activeSetupTab.dataset.setupTabTarget);
|
|
}
|
|
|
|
window.NexusModal = (() => {
|
|
const openModals = new Set();
|
|
|
|
const syncBodyState = () => {
|
|
document.body.classList.toggle('has-modal-open', openModals.size > 0);
|
|
};
|
|
|
|
const create = (root, options = {}) => {
|
|
if (!root) {
|
|
return null;
|
|
}
|
|
|
|
const modal = {
|
|
root,
|
|
isOpen: () => root.classList.contains('is-open'),
|
|
open() {
|
|
root.classList.add('is-open');
|
|
root.setAttribute('aria-hidden', 'false');
|
|
openModals.add(root);
|
|
syncBodyState();
|
|
if (typeof options.onOpen === 'function') {
|
|
options.onOpen();
|
|
}
|
|
const focusTarget = options.initialFocus ? root.querySelector(options.initialFocus) : null;
|
|
if (focusTarget instanceof HTMLElement) {
|
|
window.setTimeout(() => focusTarget.focus(), 20);
|
|
}
|
|
},
|
|
close() {
|
|
root.classList.remove('is-open');
|
|
root.setAttribute('aria-hidden', 'true');
|
|
openModals.delete(root);
|
|
syncBodyState();
|
|
if (typeof options.onClose === 'function') {
|
|
options.onClose();
|
|
}
|
|
},
|
|
};
|
|
|
|
if (!root.dataset.modalBound) {
|
|
root.addEventListener('click', (event) => {
|
|
if (event.target === root) {
|
|
modal.close();
|
|
}
|
|
});
|
|
root.dataset.modalBound = '1';
|
|
}
|
|
|
|
return modal;
|
|
};
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key !== 'Escape') {
|
|
return;
|
|
}
|
|
const active = Array.from(openModals).pop();
|
|
if (!active) {
|
|
return;
|
|
}
|
|
active.classList.remove('is-open');
|
|
active.setAttribute('aria-hidden', 'true');
|
|
openModals.delete(active);
|
|
syncBodyState();
|
|
});
|
|
|
|
return { create };
|
|
})();
|