Skip to main content
Glama
ThemeManager.ts10.8 kB
/** * Theme Management System * * CSS custom properties (CSS variables) for themeable values. * No framework dependencies - pure CSS + TypeScript. * * Features: * - Dark/Light mode presets * - Custom theme registration * - CSS variable generation * - Client-side localStorage persistence */ export interface Theme { name: string; displayName: string; variables: Record<string, string>; // CSS custom properties } /** * Dark theme preset (default) * Optimized for low-light environments, reduced eye strain */ export const DARK_THEME: Theme = { name: 'dark', displayName: 'Dark Mode', variables: { // Background colors '--bg-primary': '#1e1e1e', '--bg-secondary': '#252526', '--bg-tertiary': '#2d2d30', // Text colors '--text-primary': '#d4d4d4', '--text-secondary': '#858585', '--text-muted': '#5c5c5c', // Accent colors '--accent': '#007acc', '--accent-hover': '#005a9e', '--accent-active': '#004578', // Border colors '--border': '#3c3c3c', '--border-focus': '#007acc', // Semantic colors '--success': '#16825d', '--warning': '#f9a825', '--error': '#d32f2f', '--info': '#2196f3', // Node type colors (for signal maps) '--node-eventbus': '#1f77b4', '--node-signalbus': '#ff7f0e', '--node-component': '#2ca02c', '--node-unknown': '#999999', // Graph visualization '--link-color': '#999999', '--link-opacity': '0.6', '--cluster-fill-opacity': '0.1', '--cluster-stroke': '#007acc', // UI component colors '--button-bg': '#007acc', '--button-hover': '#005a9e', '--input-bg': '#3c3c3c', '--input-border': '#5c5c5c', // Shadows '--shadow-sm': '0 1px 2px rgba(0, 0, 0, 0.3)', '--shadow-md': '0 4px 6px rgba(0, 0, 0, 0.4)', '--shadow-lg': '0 10px 15px rgba(0, 0, 0, 0.5)' } }; /** * Light theme preset * Optimized for bright environments, high contrast */ export const LIGHT_THEME: Theme = { name: 'light', displayName: 'Light Mode', variables: { // Background colors '--bg-primary': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#e0e0e0', // Text colors '--text-primary': '#1e1e1e', '--text-secondary': '#5c5c5c', '--text-muted': '#858585', // Accent colors '--accent': '#0066cc', '--accent-hover': '#004499', '--accent-active': '#003366', // Border colors '--border': '#d4d4d4', '--border-focus': '#0066cc', // Semantic colors '--success': '#1a8754', '--warning': '#f57c00', '--error': '#c62828', '--info': '#1976d2', // Node type colors (for signal maps) '--node-eventbus': '#2563eb', '--node-signalbus': '#ea580c', '--node-component': '#16a34a', '--node-unknown': '#666666', // Graph visualization '--link-color': '#666666', '--link-opacity': '0.4', '--cluster-fill-opacity': '0.1', '--cluster-stroke': '#0066cc', // UI component colors '--button-bg': '#0066cc', '--button-hover': '#004499', '--input-bg': '#ffffff', '--input-border': '#d4d4d4', // Shadows '--shadow-sm': '0 1px 2px rgba(0, 0, 0, 0.1)', '--shadow-md': '0 4px 6px rgba(0, 0, 0, 0.15)', '--shadow-lg': '0 10px 15px rgba(0, 0, 0, 0.2)' } }; /** * Theme Manager * * Manages theme registration, CSS generation, and preference loading. */ export class ThemeManager { private themes: Map<string, Theme> = new Map(); constructor() { // Register built-in themes this.registerTheme(DARK_THEME); this.registerTheme(LIGHT_THEME); } /** * Register a custom theme * * @param theme Theme definition with CSS variables */ registerTheme(theme: Theme): void { if (!theme.name || theme.name.trim() === '') { throw new Error('Theme name is required'); } if (!theme.displayName || theme.displayName.trim() === '') { throw new Error('Theme displayName is required'); } if (!theme.variables || Object.keys(theme.variables).length === 0) { throw new Error('Theme variables are required'); } this.themes.set(theme.name, theme); } /** * Get theme by name * * @param themeName Theme identifier * @returns Theme object or undefined if not found */ getTheme(themeName: string): Theme | undefined { return this.themes.get(themeName); } /** * Get all registered theme names * * @returns Array of theme names */ getAvailableThemes(): string[] { return Array.from(this.themes.keys()); } /** * Generate CSS with theme variables for embedding in HTML * * @param themeName Theme to generate CSS for (defaults to 'dark') * @returns CSS string with :root variables */ generateThemeCSS(themeName: string = 'dark'): string { const theme = this.themes.get(themeName) || DARK_THEME; // Generate CSS custom properties const cssVars = Object.entries(theme.variables) .map(([key, value]) => ` ${key}: ${value};`) .join('\n'); return ` <style id="theme-css"> :root { ${cssVars} } /* Base styles using CSS variables */ body { background: var(--bg-primary); color: var(--text-primary); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; } /* UI components */ .filter-panel, .search-box, .zoom-controls, .controls-panel { background: var(--bg-secondary); border: 1px solid var(--border); color: var(--text-primary); border-radius: 4px; padding: 12px; } button { background: var(--button-bg); color: #ffffff; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s ease; } button:hover { background: var(--button-hover); } button:active { background: var(--accent-active); } button:disabled { opacity: 0.5; cursor: not-allowed; } input[type="text"], input[type="search"] { background: var(--input-bg); border: 1px solid var(--input-border); color: var(--text-primary); padding: 8px; border-radius: 4px; font-size: 14px; } input:focus { outline: none; border-color: var(--border-focus); } /* Node type colors (for D3 visualizations) */ .node-eventbus { fill: var(--node-eventbus); } .node-signalbus { fill: var(--node-signalbus); } .node-component { fill: var(--node-component); } .node-unknown { fill: var(--node-unknown); } /* Graph links */ .link { stroke: var(--link-color); stroke-opacity: var(--link-opacity); } /* Cluster boundaries */ .cluster-boundary { fill-opacity: var(--cluster-fill-opacity); stroke: var(--cluster-stroke); } /* Semantic color utilities */ .text-success { color: var(--success); } .text-warning { color: var(--warning); } .text-error { color: var(--error); } .text-info { color: var(--info); } .text-muted { color: var(--text-muted); } </style> `.trim(); } /** * Generate theme switcher UI (embedded HTML) * * @returns HTML string with theme switcher buttons */ generateThemeSwitcherHTML(): string { const themeButtons = Array.from(this.themes.values()) .map(theme => { const icon = theme.name === 'dark' ? '🌙' : '☀️'; return `<button onclick="setTheme('${theme.name}')" id="theme-${theme.name}" class="theme-btn">${icon} ${theme.displayName}</button>`; }) .join('\n '); // Serialize themes for client-side switching const themesJSON = JSON.stringify( Array.from(this.themes.values()).reduce((acc, theme) => { acc[theme.name] = theme.variables; return acc; }, {} as Record<string, Record<string, string>>) ); return ` <div class="theme-switcher" style="position: fixed; top: 10px; right: 10px; z-index: 1000; display: flex; gap: 8px;"> ${themeButtons} </div> <script> // Theme data (embedded from server) const THEMES = ${themesJSON}; /** * Set theme by applying CSS variables * @param {string} themeName - Theme identifier */ function setTheme(themeName) { const theme = THEMES[themeName]; if (!theme) { console.error('Unknown theme:', themeName); return; } // Apply CSS variables to :root Object.entries(theme).forEach(([key, value]) => { document.documentElement.style.setProperty(key, value); }); // Update active button styling document.querySelectorAll('.theme-btn').forEach(btn => { btn.classList.remove('active'); btn.style.opacity = '0.7'; }); const activeBtn = document.getElementById('theme-' + themeName); if (activeBtn) { activeBtn.classList.add('active'); activeBtn.style.opacity = '1'; activeBtn.style.fontWeight = 'bold'; } // Persist to localStorage (client-side only) try { localStorage.setItem('cts-theme-preference', themeName); } catch (e) { console.warn('Failed to save theme preference:', e); } } /** * Load saved theme preference on page load */ window.addEventListener('DOMContentLoaded', () => { let savedTheme = 'dark'; // Default try { savedTheme = localStorage.getItem('cts-theme-preference') || 'dark'; } catch (e) { console.warn('Failed to load theme preference:', e); } setTheme(savedTheme); }); </script> `.trim(); } /** * Get user's theme preference (server-side placeholder) * * NOTE: MCP stdio mode has no user context. * Preferences stored in browser localStorage only. * * @returns Default theme name */ getUserPreference(): string { // Server-side default (client will override from localStorage) return 'dark'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EricA1019/CTS_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server