- Enable `web` command to start UI server - Centralize web server setup and static serving - Implement modular UI for chat and script editor - Refactor Heroprompt UI into its own module - Introduce dynamic theme switching and mobile menu
236 lines
6.2 KiB
JavaScript
236 lines
6.2 KiB
JavaScript
/**
|
|
* Theme Management for Admin UI
|
|
* Handles light/dark theme switching with localStorage persistence
|
|
*/
|
|
|
|
class ThemeManager {
|
|
constructor() {
|
|
this.currentTheme = this.getStoredTheme() || this.getPreferredTheme();
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize theme manager
|
|
*/
|
|
init() {
|
|
this.applyTheme(this.currentTheme);
|
|
this.createThemeToggle();
|
|
this.bindEvents();
|
|
}
|
|
|
|
/**
|
|
* Get theme from localStorage
|
|
*/
|
|
getStoredTheme() {
|
|
return localStorage.getItem('admin-theme');
|
|
}
|
|
|
|
/**
|
|
* Get user's preferred theme from system
|
|
*/
|
|
getPreferredTheme() {
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return 'dark';
|
|
}
|
|
return 'light';
|
|
}
|
|
|
|
/**
|
|
* Store theme preference
|
|
*/
|
|
setStoredTheme(theme) {
|
|
localStorage.setItem('admin-theme', theme);
|
|
}
|
|
|
|
/**
|
|
* Apply theme to document
|
|
*/
|
|
applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
this.currentTheme = theme;
|
|
this.setStoredTheme(theme);
|
|
this.updateToggleButton();
|
|
}
|
|
|
|
/**
|
|
* Toggle between light and dark themes
|
|
*/
|
|
toggleTheme() {
|
|
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
|
|
this.applyTheme(newTheme);
|
|
}
|
|
|
|
/**
|
|
* Create theme toggle button
|
|
*/
|
|
createThemeToggle() {
|
|
const toggle = document.createElement('button');
|
|
toggle.className = 'theme-toggle';
|
|
toggle.id = 'theme-toggle';
|
|
toggle.setAttribute('aria-label', 'Toggle theme');
|
|
toggle.setAttribute('title', 'Toggle light/dark theme');
|
|
|
|
document.body.appendChild(toggle);
|
|
}
|
|
|
|
/**
|
|
* Update toggle button text and icon
|
|
*/
|
|
updateToggleButton() {
|
|
const toggle = document.getElementById('theme-toggle');
|
|
if (toggle) {
|
|
const icon = this.currentTheme === 'light' ? '🌙' : '☀️';
|
|
const text = this.currentTheme === 'light' ? 'Dark' : 'Light';
|
|
toggle.innerHTML = `${icon} ${text}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bind event listeners
|
|
*/
|
|
bindEvents() {
|
|
// Theme toggle button click
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.id === 'theme-toggle') {
|
|
this.toggleTheme();
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcut (Ctrl/Cmd + Shift + T)
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'T') {
|
|
e.preventDefault();
|
|
this.toggleTheme();
|
|
}
|
|
});
|
|
|
|
// Listen for system theme changes
|
|
if (window.matchMedia) {
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
if (!this.getStoredTheme()) {
|
|
this.applyTheme(e.matches ? 'dark' : 'light');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current theme
|
|
*/
|
|
getCurrentTheme() {
|
|
return this.currentTheme;
|
|
}
|
|
|
|
/**
|
|
* Set specific theme
|
|
*/
|
|
setTheme(theme) {
|
|
if (theme === 'light' || theme === 'dark') {
|
|
this.applyTheme(theme);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mobile Menu Management
|
|
*/
|
|
class MobileMenuManager {
|
|
constructor() {
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createMobileToggle();
|
|
this.bindEvents();
|
|
}
|
|
|
|
createMobileToggle() {
|
|
const toggle = document.createElement('button');
|
|
toggle.className = 'mobile-menu-toggle';
|
|
toggle.id = 'mobile-menu-toggle';
|
|
toggle.innerHTML = '☰ Menu';
|
|
toggle.setAttribute('aria-label', 'Toggle navigation menu');
|
|
|
|
document.body.appendChild(toggle);
|
|
}
|
|
|
|
bindEvents() {
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.id === 'mobile-menu-toggle') {
|
|
this.toggleMobileMenu();
|
|
}
|
|
});
|
|
|
|
// Close menu when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
const toggle = document.getElementById('mobile-menu-toggle');
|
|
|
|
if (sidebar && sidebar.classList.contains('show') &&
|
|
!sidebar.contains(e.target) &&
|
|
e.target !== toggle) {
|
|
this.closeMobileMenu();
|
|
}
|
|
});
|
|
|
|
// Close menu on escape key
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
this.closeMobileMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
toggleMobileMenu() {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('show');
|
|
}
|
|
}
|
|
|
|
closeMobileMenu() {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (sidebar) {
|
|
sidebar.classList.remove('show');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize when DOM is ready
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize theme manager
|
|
window.themeManager = new ThemeManager();
|
|
|
|
// Initialize mobile menu manager
|
|
window.mobileMenuManager = new MobileMenuManager();
|
|
|
|
// Add smooth scrolling to menu links
|
|
document.querySelectorAll('.menu-leaf a').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
// Close mobile menu when link is clicked
|
|
window.mobileMenuManager.closeMobileMenu();
|
|
});
|
|
});
|
|
|
|
// Enhance menu collapse animations
|
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(toggle => {
|
|
toggle.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const target = document.querySelector(toggle.getAttribute('href'));
|
|
if (target) {
|
|
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
|
|
toggle.setAttribute('aria-expanded', !isExpanded);
|
|
target.classList.toggle('show');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Export for external use
|
|
*/
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { ThemeManager, MobileMenuManager };
|
|
} |