-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Theme loading, dynamic stylesheet injection, theme indexing, and page loader transitions.
*/
import { queryEl, queryAll } from './dom-utils.js';
export const DEFAULT_THEME = 'theme_dark';
export const DEFAULT_THEME_FAMILY = 'DARK';
const HLJS_VERSION = '11.12.0';
/**
* Builds the highlight.js theme stylesheet CDN URL for the given style.
* @param {string} style - The highlight.js style name ('default' or 'dark').
* @returns {string} The CDN stylesheet URL.
*/
export function get_hljs_css_url(style) {
return `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${HLJS_VERSION}/styles/${style}.min.css`;
}
let cachedThemeIndex = null;
let lastAppliedTheme = null;
/**
* Fetches and caches the themes index from themes/theme_index.json.
* @returns {Promise<object>} The theme index mapping families to lists of themes.
*/
export async function fetchThemeIndex() {
if (cachedThemeIndex) {
return cachedThemeIndex;
}
try {
const response = await fetch('themes/theme_index.json');
if (response.ok) {
cachedThemeIndex = await response.json();
return cachedThemeIndex;
}
} catch (e) {
console.warn('Failed to fetch theme_index.json, using fallback', e);
}
// Fallback structure
cachedThemeIndex = {
DARK: {
name: 'Dark',
themes: [{ key: 'theme_dark', name: 'Dark' }]
},
LIGHT: {
name: 'Light',
themes: [{ key: 'theme_light', name: 'Light' }]
}
};
return cachedThemeIndex;
}
/**
* Returns a flat array of all valid theme keys across all families.
* @param {object} [themeIndex=null] - Optional theme index object.
* @returns {string[]} Array of theme keys.
*/
export function getAllThemeKeys(themeIndex = null) {
const index = themeIndex || cachedThemeIndex;
if (!index || typeof index !== 'object') return [DEFAULT_THEME];
const keys = [];
for (const familyKey in index) {
if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
const family = index[familyKey];
if (Array.isArray(family.themes)) {
family.themes.forEach(t => {
if (t && t.key) keys.push(t.key);
});
}
}
}
return keys;
}
/**
* Finds the family key (e.g. 'DARK', 'GRAY', 'LIGHT') that contains the given theme key.
* @param {string} themeKey - The theme key to search for.
* @param {object} [themeIndex=null] - Optional theme index.
* @returns {string} The family key, or DEFAULT_THEME_FAMILY if not found.
*/
export function getThemeFamilyForTheme(themeKey, themeIndex = null) {
const index = themeIndex || cachedThemeIndex;
if (!index || typeof index !== 'object') return DEFAULT_THEME_FAMILY;
for (const familyKey in index) {
if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
const family = index[familyKey];
if (Array.isArray(family.themes)) {
const match = family.themes.find(t => t.key === themeKey);
if (match) return familyKey;
}
}
}
return DEFAULT_THEME_FAMILY;
}
/**
* Populates a select dropdown with theme families.
* @param {HTMLSelectElement|null} selectEl - Target select element for families.
* @param {object} themeIndex - Theme index map.
* @param {string} [selectedFamily='DARK'] - Selected family key.
*/
export function populateThemeFamilyDropdown(selectEl, themeIndex, selectedFamily = DEFAULT_THEME_FAMILY) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!themeIndex || typeof themeIndex !== 'object') return;
for (const key in themeIndex) {
if (Object.prototype.hasOwnProperty.call(themeIndex, key)) {
const family = themeIndex[key];
const option = document.createElement('option');
option.value = key;
option.textContent = family.name || key;
if (key === selectedFamily) {
option.selected = true;
}
selectEl.appendChild(option);
}
}
}
/**
* Populates a select dropdown with themes for a specific theme family.
* @param {HTMLSelectElement|null} selectEl - Target select element for themes.
* @param {object} themeIndex - Theme index map.
* @param {string} familyKey - Family key ('DARK' | 'GRAY' | 'LIGHT').
* @param {string} [selectedTheme=''] - Selected theme key.
*/
export function populateThemeDropdown(selectEl, themeIndex, familyKey, selectedTheme = '') {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!themeIndex || typeof themeIndex !== 'object') return;
const family = themeIndex[familyKey];
if (!family || !Array.isArray(family.themes)) return;
family.themes.forEach(t => {
const option = document.createElement('option');
option.value = t.key;
option.textContent = t.name || t.key;
if (t.key === selectedTheme) {
option.selected = true;
}
selectEl.appendChild(option);
});
}
/**
* Fades out and hides the page loader element.
* @param {HTMLElement|null} [loaderEl=null] - Optional loader element reference.
*/
export function fadeOutLoader(loaderEl = null) {
const loader = loaderEl || queryEl('.page-loader');
if (loader) {
loader.style.opacity = '0';
loader.addEventListener('transitionend', () => {
loader.style.display = 'none';
}, { once: true });
}
}
/**
* Fades out the page loader once the panels layout transition has finished,
* so the layout animation happens behind the loader. Falls back to fading
* immediately when the layout did not change.
* @param {boolean} layoutChanged - Whether the panels layout classes changed during initialization.
*/
export function fadeOutLoaderAfterLayout(layoutChanged) {
const loader = queryEl('.page-loader');
if (!loader) return;
if (!layoutChanged) {
fadeOutLoader(loader);
return;
}
const layoutBody = queryEl('.div-structure-body');
const fallback = setTimeout(() => fadeOutLoader(loader), 500);
const onLayoutTransitionEnd = (e) => {
if (e.target !== layoutBody || e.propertyName !== 'grid-template-columns') return;
clearTimeout(fallback);
layoutBody.removeEventListener('transitionend', onLayoutTransitionEnd);
fadeOutLoader(loader);
};
layoutBody.addEventListener('transitionend', onLayoutTransitionEnd);
}
/**
* Applies a theme by fetching root variable declarations from dynamic.php,
* updating the #theme-variables style element, and transitioning smoothly via View Transitions API.
* Re-applying the already active theme is skipped, so the transition only runs on a real theme change.
* @param {string} theme - The theme identifier.
* @param {boolean} [includeHljs=true] - Whether to also swap the Highlight.js theme stylesheet.
* @param {Function} [onComplete] - Optional callback once theme stylesheet finishes applying.
*/
export async function applyTheme(theme, includeHljs = true, onComplete = null) {
const activeTheme = theme || DEFAULT_THEME;
const loader = queryEl('.page-loader');
try {
const response = await fetch(`dynamic.php?t=${encodeURIComponent(activeTheme)}`);
if (!response.ok) {
throw new Error(`Failed to fetch theme variables: ${response.status}`);
}
const cssText = await response.text();
const updateDom = () => {
let styleEl = document.getElementById('theme-variables');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'theme-variables';
document.head.appendChild(styleEl);
}
styleEl.textContent = cssText;
lastAppliedTheme = activeTheme;
void document.documentElement.offsetHeight;
if (includeHljs) {
const hljsStyle = activeTheme.includes('light') ? 'default' : 'dark';
const hljsTheme = get_hljs_css_url(hljsStyle);
const existingHljs = queryEl('link[href*="highlight.js"]');
if (existingHljs && existingHljs.getAttribute('href') !== hljsTheme) {
existingHljs.setAttribute('href', hljsTheme);
}
}
};
const isLoaderVisible = loader && getComputedStyle(loader).display !== 'none' && getComputedStyle(loader).opacity !== '0';
const finish = () => {
if (typeof onComplete === 'function') onComplete(activeTheme);
};
// the active theme was re-requested (e.g. boot calls applyTheme twice);
// only an actual theme change may run the fade transition
if (activeTheme === lastAppliedTheme) {
finish();
return;
}
if (document.startViewTransition && !isLoaderVisible) {
document.documentElement.classList.add('theme-transitioning');
const transition = document.startViewTransition(() => {
updateDom();
});
transition.finished.finally(() => {
document.documentElement.classList.remove('theme-transitioning');
finish();
});
} else {
updateDom();
finish();
}
} catch (e) {
console.warn('Failed to apply theme dynamically', e);
if (typeof onComplete === 'function') onComplete(activeTheme);
}
}
theme-utils.js
×
Type: Web, text/x-java
8.34 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
8.34 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File