Skip to main content
Glama
background-controls.ts17.3 kB
/** * Background Controls System for HTML Visualizations * Provides interactive background switching and color picker functionality */ import Handlebars from 'handlebars'; export interface BackgroundControlConfig { enableToggle: boolean; enableColorPicker: boolean; defaultBackground: 'light' | 'dark' | 'auto'; customColors: string[]; position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; accessibility: { keyboardNavigation: boolean; screenReaderSupport: boolean; highContrast: boolean; }; } export const defaultBackgroundConfig: BackgroundControlConfig = { enableToggle: true, enableColorPicker: true, defaultBackground: 'light', customColors: [ '#ffffff', '#f8f9fa', '#e9ecef', '#1a1a1a', '#212529', '#343a40', ], position: 'top-right', accessibility: { keyboardNavigation: true, screenReaderSupport: true, highContrast: true, }, }; /** * Register Handlebars helpers for background controls */ export function registerBackgroundControlHelpers(): void { // Helper to generate background toggle button Handlebars.registerHelper( 'backgroundToggle', function (config: Partial<BackgroundControlConfig> = {}) { const mergedConfig = { ...defaultBackgroundConfig, ...config }; if (!mergedConfig.enableToggle) { return ''; } return new Handlebars.SafeString(` <button id="background-toggle-btn" class="background-control-btn toggle-btn" type="button" aria-label="Toggle between light and dark backgrounds" aria-pressed="false" title="Toggle background (Alt+T)" data-keyboard-shortcut="Alt+T"> <span class="toggle-icon light-icon" aria-hidden="true">☀️</span> <span class="toggle-icon dark-icon" aria-hidden="true">🌙</span> <span class="sr-only">Toggle background theme</span> </button> `); } ); // Helper to generate color picker Handlebars.registerHelper( 'backgroundColorPicker', function (config: Partial<BackgroundControlConfig> = {}) { const mergedConfig = { ...defaultBackgroundConfig, ...config }; if (!mergedConfig.enableColorPicker) { return ''; } const presetColors = mergedConfig.customColors .map( color => `<button class="preset-color-btn" data-color="${color}" style="background-color: ${color};" aria-label="Set background to ${color}" title="${color}"></button>` ) .join(''); return new Handlebars.SafeString(` <div class="background-color-picker" role="group" aria-label="Background color picker"> <button id="color-picker-toggle" class="background-control-btn picker-toggle-btn" type="button" aria-expanded="false" aria-controls="color-picker-panel" aria-label="Open background color picker" title="Choose background color"> <span class="picker-icon" aria-hidden="true">🎨</span> <span class="sr-only">Background color picker</span> </button> <div id="color-picker-panel" class="color-picker-panel" role="dialog" aria-label="Background color selection" aria-hidden="true"> <div class="color-picker-header"> <h3 id="color-picker-title">Background Color</h3> <button class="close-picker-btn" type="button" aria-label="Close color picker" title="Close (Escape)"> ✕ </button> </div> <div class="color-picker-content"> <div class="preset-colors" role="group" aria-label="Preset colors"> <h4>Preset Colors</h4> <div class="preset-grid"> ${presetColors} </div> </div> <div class="custom-color-section"> <label for="custom-color-input">Custom Color:</label> <div class="custom-color-controls"> <input type="color" id="custom-color-input" class="custom-color-input" value="#ffffff" aria-label="Select custom background color"> <input type="text" id="custom-color-text" class="custom-color-text" placeholder="#ffffff" pattern="^#[0-9A-Fa-f]{6}$" aria-label="Enter hex color value"> </div> </div> <div class="color-picker-actions"> <button id="reset-background-btn" class="reset-btn" type="button" aria-label="Reset to default background"> Reset </button> <button id="apply-color-btn" class="apply-btn" type="button" aria-label="Apply selected color"> Apply </button> </div> </div> </div> </div> `); } ); // Helper to generate accessibility warning Handlebars.registerHelper('accessibilityWarning', function () { return new Handlebars.SafeString(` <div id="accessibility-warning" class="accessibility-warning" role="alert" aria-live="polite" style="display: none;"> <span class="warning-icon" aria-hidden="true">⚠️</span> <span class="warning-text"></span> <button class="dismiss-warning-btn" type="button" aria-label="Dismiss warning"> ✕ </button> </div> `); }); // Helper to generate complete background controls container Handlebars.registerHelper( 'backgroundControls', function (config: Partial<BackgroundControlConfig> = {}) { const mergedConfig = { ...defaultBackgroundConfig, ...config }; const positionClass = `controls-${mergedConfig.position}`; const toggleButton = mergedConfig.enableToggle ? ( Handlebars.helpers['backgroundToggle'] as ( config: unknown ) => string )?.(config) || '' : ''; const colorPicker = mergedConfig.enableColorPicker ? ( Handlebars.helpers['backgroundColorPicker'] as ( config: unknown ) => string )?.(config) || '' : ''; const accessibilityWarning = (Handlebars.helpers['accessibilityWarning'] as () => string)?.() || ''; return new Handlebars.SafeString(` <div class="background-controls ${positionClass}" role="toolbar" aria-label="Background controls" data-default-background="${mergedConfig.defaultBackground}"> ${toggleButton} ${colorPicker} ${accessibilityWarning} <div class="controls-help" style="display: none;"> <h4>Keyboard Shortcuts</h4> <ul> <li><kbd>Alt+T</kbd> - Toggle background</li> <li><kbd>Alt+C</kbd> - Open color picker</li> <li><kbd>Escape</kbd> - Close color picker</li> <li><kbd>Tab</kbd> - Navigate controls</li> </ul> </div> </div> `); } ); // Helper to check if background controls should be included Handlebars.registerHelper( 'shouldIncludeBackgroundControls', function ( config?: Partial<BackgroundControlConfig>, options?: { hash: Partial<BackgroundControlConfig> } ) { // Handle both parameter and hash-based calls const actualConfig = config || options?.hash || {}; const mergedConfig = { ...defaultBackgroundConfig, ...actualConfig }; return mergedConfig.enableToggle || mergedConfig.enableColorPicker; } ); // Helper to generate CSS custom properties for background controls Handlebars.registerHelper( 'backgroundControlsCSS', function (config: Partial<BackgroundControlConfig> = {}) { const mergedConfig = { ...defaultBackgroundConfig, ...config }; return new Handlebars.SafeString(` :root { --bg-controls-position: ${mergedConfig.position}; --bg-controls-default: ${mergedConfig.defaultBackground === 'light' ? '#ffffff' : '#1a1a1a'}; --bg-controls-accent: ${mergedConfig.defaultBackground === 'light' ? '#2563eb' : '#60a5fa'}; --bg-controls-text: ${mergedConfig.defaultBackground === 'light' ? '#1e293b' : '#f1f5f9'}; --bg-controls-border: ${mergedConfig.defaultBackground === 'light' ? '#e2e8f0' : '#374151'}; --bg-controls-shadow: ${ mergedConfig.defaultBackground === 'light' ? 'rgba(0, 0, 0, 0.1)' : 'rgba(0, 0, 0, 0.3)' }; } `); } ); } /** * Generate CSS for background controls */ export function generateBackgroundControlsCSS( _config: Partial<BackgroundControlConfig> = {} ): string { return ` /* Background Controls Styles */ .background-controls { position: fixed; z-index: 1000; display: flex; gap: 0.5rem; padding: 0.75rem; background: var(--bg-controls-default); border: 1px solid var(--bg-controls-border); border-radius: 0.5rem; box-shadow: 0 4px 6px -1px var(--bg-controls-shadow); transition: all 0.2s ease-in-out; } .background-controls.controls-top-right { top: 1rem; right: 1rem; } .background-controls.controls-top-left { top: 1rem; left: 1rem; } .background-controls.controls-bottom-right { bottom: 1rem; right: 1rem; } .background-controls.controls-bottom-left { bottom: 1rem; left: 1rem; } .background-control-btn { display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; padding: 0; background: transparent; border: 1px solid var(--bg-controls-border); border-radius: 0.375rem; color: var(--bg-controls-text); cursor: pointer; transition: all 0.15s ease-in-out; font-size: 1.125rem; } .background-control-btn:hover { background: var(--bg-controls-accent); color: white; transform: translateY(-1px); box-shadow: 0 2px 4px -1px var(--bg-controls-shadow); } .background-control-btn:focus { outline: 2px solid var(--bg-controls-accent); outline-offset: 2px; } .background-control-btn:active { transform: translateY(0); } /* Toggle Button Styles */ .toggle-btn[aria-pressed="false"] .dark-icon { display: none; } .toggle-btn[aria-pressed="true"] .light-icon { display: none; } /* Color Picker Styles */ .background-color-picker { position: relative; } .color-picker-panel { position: absolute; top: calc(100% + 0.5rem); right: 0; width: 20rem; max-width: 90vw; background: var(--bg-controls-default); border: 1px solid var(--bg-controls-border); border-radius: 0.5rem; box-shadow: 0 10px 15px -3px var(--bg-controls-shadow); opacity: 0; visibility: hidden; transform: translateY(-0.5rem); transition: all 0.2s ease-in-out; z-index: 1001; } .color-picker-panel[aria-hidden="false"] { opacity: 1; visibility: visible; transform: translateY(0); } .color-picker-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem; border-bottom: 1px solid var(--bg-controls-border); } .color-picker-header h3 { margin: 0; font-size: 1rem; font-weight: 600; color: var(--bg-controls-text); } .close-picker-btn { display: flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem; padding: 0; background: transparent; border: none; border-radius: 0.25rem; color: var(--bg-controls-text); cursor: pointer; font-size: 0.875rem; } .close-picker-btn:hover { background: var(--bg-controls-border); } .color-picker-content { padding: 1rem; } .preset-colors h4 { margin: 0 0 0.75rem 0; font-size: 0.875rem; font-weight: 500; color: var(--bg-controls-text); } .preset-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.5rem; margin-bottom: 1rem; } .preset-color-btn { width: 2rem; height: 2rem; border: 2px solid var(--bg-controls-border); border-radius: 0.25rem; cursor: pointer; transition: all 0.15s ease-in-out; } .preset-color-btn:hover { transform: scale(1.1); box-shadow: 0 2px 4px -1px var(--bg-controls-shadow); } .preset-color-btn:focus { outline: 2px solid var(--bg-controls-accent); outline-offset: 2px; } .custom-color-section { margin-bottom: 1rem; } .custom-color-section label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 500; color: var(--bg-controls-text); } .custom-color-controls { display: flex; gap: 0.5rem; } .custom-color-input { width: 3rem; height: 2.5rem; border: 1px solid var(--bg-controls-border); border-radius: 0.375rem; cursor: pointer; } .custom-color-text { flex: 1; height: 2.5rem; padding: 0 0.75rem; border: 1px solid var(--bg-controls-border); border-radius: 0.375rem; background: transparent; color: var(--bg-controls-text); font-family: monospace; } .custom-color-text:focus { outline: 2px solid var(--bg-controls-accent); outline-offset: -2px; border-color: var(--bg-controls-accent); } .color-picker-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } .reset-btn, .apply-btn { padding: 0.5rem 1rem; border: 1px solid var(--bg-controls-border); border-radius: 0.375rem; background: transparent; color: var(--bg-controls-text); cursor: pointer; font-size: 0.875rem; transition: all 0.15s ease-in-out; } .apply-btn { background: var(--bg-controls-accent); color: white; border-color: var(--bg-controls-accent); } .reset-btn:hover, .apply-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 4px -1px var(--bg-controls-shadow); } /* Accessibility Warning Styles */ .accessibility-warning { position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; background: #fbbf24; color: #92400e; border: 1px solid #f59e0b; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); z-index: 1002; max-width: 90vw; font-size: 0.875rem; } .warning-icon { font-size: 1rem; } .dismiss-warning-btn { display: flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; padding: 0; background: transparent; border: none; border-radius: 0.25rem; color: #92400e; cursor: pointer; font-size: 0.75rem; } .dismiss-warning-btn:hover { background: rgba(146, 64, 14, 0.1); } /* Screen Reader Only Content */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* High Contrast Mode Support */ @media (prefers-contrast: high) { .background-controls { border-width: 2px; } .background-control-btn { border-width: 2px; } .background-control-btn:focus { outline-width: 3px; } } /* Reduced Motion Support */ @media (prefers-reduced-motion: reduce) { .background-controls, .background-control-btn, .color-picker-panel, .preset-color-btn { transition: none; } } /* Mobile Responsive */ @media (max-width: 768px) { .background-controls { padding: 0.5rem; gap: 0.375rem; } .background-control-btn { width: 2.25rem; height: 2.25rem; font-size: 1rem; } .color-picker-panel { width: 18rem; right: -1rem; } } /* Dark Mode Adjustments */ [data-background-theme="dark"] { --bg-controls-default: #1a1a1a; --bg-controls-accent: #60a5fa; --bg-controls-text: #f1f5f9; --bg-controls-border: #374151; --bg-controls-shadow: rgba(0, 0, 0, 0.3); } `; }

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/keyurgolani/ColorMcp'

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