/** * Spaxel Dashboard - Command Palette (Ctrl+K / Cmd+K) * * Universal search and command interface for power users. * Fuzzy matching across zones, people, nodes, events, settings, and help topics. */ (function() { 'use strict'; // ============================================ // Configuration // ============================================ const STORAGE_KEY_RECENT = 'spaxel_command_recent'; const STORAGE_KEY_HINT_DISMISSED = 'spaxel_command_hint_dismissed'; const MAX_RECENT = 5; const MIN_SEARCH_LENGTH = 1; // ============================================ // State // ============================================ let isOpen = false; let selectedIndex = 0; let searchQuery = ''; let recentCommands = []; let commandRegistry = new Map(); let searchResults = []; // ============================================ // Command Registry // ============================================ /** * Register a command */ function registerCommand(id, config) { commandRegistry.set(id, { id: id, title: config.title, description: config.description || '', category: config.category || 'general', icon: config.icon || '⚙', keywords: config.keywords || [], action: config.action, shortcut: config.shortcut || null }); console.log('[Command Palette] Registered command:', id); } /** * Initialize default commands */ function initializeCommands() { // Navigation commands registerCommand('nav-live', { title: 'Live View', description: 'Go to real-time 3D detection view', category: 'navigation', icon: '■', keywords: ['live', '3d', 'view', 'home', 'dashboard'], action: () => navigateToMode('live') }); registerCommand('nav-timeline', { title: 'Timeline', description: 'View activity history and events', category: 'navigation', icon: '⏰', keywords: ['timeline', 'history', 'events', 'activity', 'log'], action: () => navigateToMode('timeline') }); registerCommand('nav-automations', { title: 'Automations', description: 'Manage spatial triggers and automation rules', category: 'navigation', icon: '⚙', keywords: ['automations', 'triggers', 'rules', 'automation'], action: () => navigateToMode('automations') }); registerCommand('nav-settings', { title: 'Settings', description: 'Open system configuration and preferences', category: 'navigation', icon: '⚙', keywords: ['settings', 'config', 'preferences', 'options'], action: () => { if (window.openSettingsPanel) { window.openSettingsPanel(); } } }); registerCommand('nav-ambient', { title: 'Ambient Mode', description: 'Switch to always-on display mode', category: 'navigation', icon: '◉', keywords: ['ambient', 'display', 'wall', 'tablet'], action: () => navigateToMode('ambient') }); registerCommand('nav-replay', { title: 'Replay Mode', description: 'Enter time-travel debugging mode', category: 'navigation', icon: '⏵', keywords: ['replay', 'debug', 'time', 'travel', 'history'], action: () => { if (window.SpaxelReplay) { window.SpaxelReplay.pauseLive(); } else { navigateToMode('replay'); } } }); registerCommand('nav-simulator', { title: 'Pre-Deployment Simulator', description: 'Open simulator for testing coverage', category: 'navigation', icon: '⚛', keywords: ['simulator', 'simulation', 'test', 'coverage', 'planning'], action: () => { if (window.Simulate) { window.Simulate.togglePanel(); } else { navigateToMode('simulate'); } } }); // Fleet commands registerCommand('fleet-update-all', { title: 'Update All Nodes', description: 'Trigger OTA firmware update for all nodes', category: 'fleet', icon: '⬆', keywords: ['update', 'ota', 'firmware', 'upgrade', 'all nodes'], action: () => confirmAndExecute('Update all nodes?', async () => { const response = await fetch('/api/nodes/update-all', { method: 'POST' }); if (response.ok) { showToast('Firmware update started for all nodes', 'success'); } else { showToast('Failed to start firmware update', 'warning'); } }) }); registerCommand('fleet-rebaseline-all', { title: 'Re-baseline All', description: 'Recalibrate detection baselines for all links', category: 'fleet', icon: '♻', keywords: ['baseline', 'calibrate', 'recalibrate', 'reset'], action: () => confirmAndExecute('Re-baseline all links?', async () => { const response = await fetch('/api/nodes/rebaseline-all', { method: 'POST' }); if (response.ok) { showToast('Re-baseline started for all links', 'success'); } else { showToast('Failed to start re-baseline', 'warning'); } }) }); registerCommand('fleet-export-config', { title: 'Export Configuration', description: 'Download full system configuration as JSON', category: 'fleet', icon: '📥', keywords: ['export', 'download', 'backup', 'save', 'config'], action: async () => { const response = await fetch('/api/export'); if (response.ok) { const config = await response.json(); const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `spaxel-config-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); showToast('Configuration exported', 'success'); } else { showToast('Failed to export configuration', 'warning'); } } }); // Security commands registerCommand('security-arm', { title: 'Arm Security Mode', description: 'Enable security alerts for any motion detection', category: 'security', icon: '🔒', keywords: ['arm', 'security', 'alert', 'enable', 'protect'], action: () => confirmAndExecute('Arm security mode?', async () => { const response = await fetch('/api/security/arm', { method: 'POST' }); if (response.ok) { showToast('Security mode armed', 'success'); } else { showToast('Failed to arm security mode', 'warning'); } }) }); registerCommand('security-disarm', { title: 'Disarm Security Mode', description: 'Disable security alerts', category: 'security', icon: '🔓', keywords: ['disarm', 'security', 'disable', 'off'], action: () => confirmAndExecute('Disarm security mode?', async () => { const response = await fetch('/api/security/disarm', { method: 'POST' }); if (response.ok) { showToast('Security mode disarmed', 'info'); } else { showToast('Failed to disarm security mode', 'warning'); } }) }); // View commands registerCommand('view-toggle-gdop', { title: 'Toggle GDOP Overlay', description: 'Show/hide coverage quality map', category: 'view', icon: '📅', keywords: ['gdop', 'coverage', 'map', 'overlay', 'quality'], action: () => { if (window.Placement) { window.Placement.toggleGDOP(); } } }); registerCommand('view-toggle-fresnel', { title: 'Toggle Fresnel Zones', description: 'Show/hide Fresnel zone visualization', category: 'view', icon: '◊', keywords: ['fresnel', 'zones', 'visualization', 'debug'], action: () => { if (window.toggleFresnelZones) { window.toggleFresnelZones(); } } }); registerCommand('view-toggle-links', { title: 'Toggle Node Links', description: 'Show/hide link visualization', category: 'view', icon: '📞', keywords: ['links', 'nodes', 'connections', 'lines'], action: () => { if (window.Viz3D) { window.Viz3D.toggleLinks(); } } }); registerCommand('view-top-down', { title: 'Top-Down View', description: 'Switch to overhead view', category: 'view', icon: '↕', keywords: ['top', 'down', 'overhead', 'plan', 'birdseye'], action: () => { if (window.Viz3D) { window.Viz3D.setViewPreset('topdown'); } } }); registerCommand('view-perspective', { title: 'Perspective View', description: 'Switch to 3D perspective view', category: 'view', icon: '🏠', keywords: ['perspective', '3d', 'angle', 'view'], action: () => { if (window.Viz3D) { window.Viz3D.setViewPreset('perspective'); } } }); // Mode commands registerCommand('mode-simple', { title: 'Switch to Simple Mode', description: 'Enable card-based mobile-first UI', category: 'mode', icon: '📱', keywords: ['simple', 'basic', 'easy', 'mobile'], action: () => { if (window.SpaxelSimpleMode) { window.SpaxelSimpleMode.enable(); } } }); registerCommand('mode-expert', { title: 'Switch to Expert Mode', description: 'Enable full 3D visualization', category: 'mode', icon: '⚙', keywords: ['expert', 'advanced', 'full', '3d'], action: () => { if (window.SpaxelSimpleMode) { window.SpaxelSimpleMode.disable(); } } }); // Help commands registerCommand('help-fall-detection', { title: 'Fall Detection Help', description: 'Learn about fall detection settings', category: 'help', icon: '❓', keywords: ['fall', 'detection', 'help', 'explain', 'how'], action: () => showHelpTopic('fall-detection') }); registerCommand('help-accuracy', { title: 'Improve Accuracy', description: 'Tips for improving detection accuracy', category: 'help', icon: '❓', keywords: ['accuracy', 'improve', 'better', 'tips', 'help'], action: () => showHelpTopic('accuracy') }); registerCommand('help-why-false-positive', { title: 'Why False Positive?', description: 'Explain most recent incorrect detection', category: 'help', icon: '❓', keywords: ['why', 'false', 'positive', 'explain', 'help'], action: () => showHelpTopic('false-positive') }); registerCommand('help-keyboard', { title: 'Keyboard Shortcuts', description: 'View all available keyboard shortcuts', category: 'help', icon: '⌨', keywords: ['keyboard', 'shortcuts', 'hotkey', 'keys', 'help'], action: () => showHelpTopic('shortcuts') }); // Theme commands registerCommand('theme-dark', { title: 'Dark Mode', description: 'Switch to dark theme', category: 'theme', icon: '🌙', keywords: ['dark', 'theme', 'night', 'mode'], action: () => setTheme('dark') }); registerCommand('theme-light', { title: 'Light Mode', description: 'Switch to light theme', category: 'theme', icon: '☀', keywords: ['light', 'theme', 'day', 'mode'], action: () => setTheme('light') }); // Node commands (dynamically populated) registerCommand('node-add', { title: 'Add New Node', description: 'Start onboarding for a new ESP32 node', category: 'nodes', icon: '➕', keywords: ['add', 'node', 'new', 'onboard', 'provision'], action: () => { if (window.SpaxelOnboard) { window.SpaxelOnboard.start(); } } }); registerCommand('node-restart', { title: 'Restart Node', description: 'Restart a specific node (select from list)', category: 'nodes', icon: '🔄', keywords: ['restart', 'reboot', 'node', 'reset'], action: () => showNodeSelector('restart') }); registerCommand('node-update', { title: 'Update Node Firmware', description: 'Trigger OTA update for a specific node', category: 'nodes', icon: '⬆', keywords: ['update', 'ota', 'firmware', 'upgrade', 'node'], action: () => showNodeSelector('update') }); // Zone commands (dynamically populated) registerCommand('zone-history', { title: 'Zone History', description: 'View occupancy history for a zone', category: 'zones', icon: '📅', keywords: ['zone', 'history', 'occupancy', 'log'], action: () => showZoneSelector('history') }); console.log('[Command Palette] Commands initialized:', commandRegistry.size); } // ============================================ // UI Creation // ============================================ /** * Create the command palette UI */ function createCommandPalette() { // Check if already exists if (document.getElementById('command-palette')) { return; } const palette = document.createElement('div'); palette.id = 'command-palette'; palette.className = 'command-palette'; palette.innerHTML = `
ESC to close
`; document.body.appendChild(palette); // Set up event listeners setupEventListeners(); console.log('[Command Palette] UI created'); } /** * Set up event listeners */ function setupEventListeners() { const palette = document.getElementById('command-palette'); const input = palette.querySelector('.command-input'); const backdrop = palette.querySelector('.command-backdrop'); // Close on backdrop click backdrop.addEventListener('click', closePalette); // Close on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isOpen) { closePalette(); } }); // Global keyboard shortcut (Ctrl+K / Cmd+K) document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); togglePalette(); } }); // Input handling input.addEventListener('input', (e) => { searchQuery = e.target.value; selectedIndex = 0; performSearch(); }); input.addEventListener('keydown', handleInputKeydown); // Result clicks palette.querySelector('.command-results').addEventListener('click', (e) => { const item = e.target.closest('.command-item'); if (item) { executeCommand(item.dataset.commandId); } }); } /** * Handle keyboard navigation in input */ function handleInputKeydown(e) { if (!isOpen) return; const results = document.querySelectorAll('.command-item'); switch (e.key) { case 'ArrowDown': e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, results.length - 1); updateSelection(); break; case 'ArrowUp': e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, 0); updateSelection(); break; case 'Enter': e.preventDefault(); if (results[selectedIndex]) { executeCommand(results[selectedIndex].dataset.commandId); } break; case 'PageDown': e.preventDefault(); selectedIndex = Math.min(selectedIndex + 5, results.length - 1); updateSelection(); break; case 'PageUp': e.preventDefault(); selectedIndex = Math.max(selectedIndex - 5, 0); updateSelection(); break; case 'Home': e.preventDefault(); selectedIndex = 0; updateSelection(); break; case 'End': e.preventDefault(); selectedIndex = results.length - 1; updateSelection(); break; } } /** * Update selection highlight */ function updateSelection() { const results = document.querySelectorAll('.command-item'); results.forEach((item, index) => { item.classList.toggle('selected', index === selectedIndex); }); // Scroll selected item into view if (results[selectedIndex]) { results[selectedIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } } // ============================================ // Palette Control // ============================================ /** * Check if command palette should be available */ function isAvailable() { // Check if simple mode is active if (window.SpaxelSimpleMode && window.SpaxelSimpleMode.isEnabled()) { return false; } // Check if ambient mode is active if (window.SpaxelAmbient && window.SpaxelAmbient.isEnabled()) { return false; } return true; } /** * Toggle command palette open/closed */ function togglePalette() { if (isOpen) { closePalette(); } else { openPalette(); } } /** * Open command palette */ function openPalette() { // Check if command palette is available in current mode if (!isAvailable()) { showToast('Command palette is only available in expert mode', 'info'); return; } createCommandPalette(); const palette = document.getElementById('command-palette'); const input = palette.querySelector('.command-input'); palette.classList.add('visible'); isOpen = true; // Focus input setTimeout(() => { input.focus(); input.select(); }, 100); // Load initial results searchQuery = ''; selectedIndex = 0; performSearch(); console.log('[Command Palette] Opened'); } /** * Close command palette */ function closePalette() { const palette = document.getElementById('command-palette'); if (palette) { palette.classList.remove('visible'); isOpen = false; // Clear input after animation setTimeout(() => { const input = palette.querySelector('.command-input'); if (input) { input.value = ''; searchQuery = ''; } }, 200); } console.log('[Command Palette] Closed'); } // ============================================ // Search // ============================================ /** * Perform fuzzy search */ function performSearch() { const resultsContainer = document.querySelector('.command-results'); const emptyState = document.querySelector('.command-empty'); if (searchQuery.length < MIN_SEARCH_LENGTH) { // Show all commands by category searchResults = getAllCommands(); } else { // Fuzzy search searchResults = fuzzySearch(searchQuery); } if (searchResults.length === 0) { resultsContainer.style.display = 'none'; emptyState.style.display = 'block'; } else { resultsContainer.style.display = 'block'; emptyState.style.display = 'none'; renderResults(searchResults); } selectedIndex = 0; updateSelection(); } /** * Get all commands organized by category */ function getAllCommands() { const commands = Array.from(commandRegistry.values()); const categories = {}; // Group by category commands.forEach(cmd => { if (!categories[cmd.category]) { categories[cmd.category] = []; } categories[cmd.category].push(cmd); }); // Convert to flat list with category headers const results = []; const categoryOrder = ['recent', 'navigation', 'fleet', 'security', 'nodes', 'zones', 'view', 'mode', 'theme', 'help']; // Add recent commands first if (recentCommands.length > 0) { results.push({ type: 'header', title: 'Recent' }); recentCommands.forEach(cmdId => { const cmd = commandRegistry.get(cmdId); if (cmd) { results.push({ type: 'command', ...cmd }); } }); } // Add categorized commands categoryOrder.forEach(category => { if (categories[category]) { results.push({ type: 'header', title: formatCategoryTitle(category) }); categories[category].forEach(cmd => { results.push({ type: 'command', ...cmd }); }); } }); return results; } /** * Fuzzy search implementation */ function fuzzySearch(query) { const searchLower = query.toLowerCase(); const commands = Array.from(commandRegistry.values()); const scored = []; commands.forEach(cmd => { let score = 0; const titleLower = cmd.title.toLowerCase(); const descLower = cmd.description.toLowerCase(); const keywordsLower = cmd.keywords.map(k => k.toLowerCase()); // Exact title match if (titleLower === searchLower) { score = 100; } // Title starts with query else if (titleLower.startsWith(searchLower)) { score = 80; } // Title contains query else if (titleLower.includes(searchLower)) { score = 60; } // Keyword match else if (keywordsLower.some(k => k.includes(searchLower))) { score = 50; } // Description contains query else if (descLower.includes(searchLower)) { score = 30; } // Fuzzy match (consecutive characters) else { const fuzzyScore = fuzzyMatch(searchLower, titleLower); if (fuzzyScore > 0) { score = fuzzyScore * 0.4; } } // Boost recent commands if (recentCommands.includes(cmd.id)) { score += 10; } if (score > 0) { scored.push({ command: cmd, score }); } }); // Sort by score and convert to results scored.sort((a, b) => b.score - a.score); // Group by category for top results const results = []; const categories = new Set(); scored.slice(0, 20).forEach(({ command: cmd }) => { if (!categories.has(cmd.category)) { categories.add(cmd.category); results.push({ type: 'header', title: formatCategoryTitle(cmd.category) }); } results.push({ type: 'command', ...cmd }); }); return results; } /** * Simple fuzzy matching */ function fuzzyMatch(query, text) { let queryIndex = 0; let textIndex = 0; let score = 0; let consecutive = 0; while (queryIndex < query.length && textIndex < text.length) { if (query[queryIndex] === text[textIndex]) { consecutive++; score += consecutive * 2; queryIndex++; } else { consecutive = 0; } textIndex++; } return queryIndex === query.length ? score : 0; } /** * Render search results */ function renderResults(results) { const container = document.querySelector('.command-results'); let html = ''; results.forEach(result => { if (result.type === 'header') { html += `
${result.title}
`; } else { html += `
${result.icon}
${highlightMatch(result.title, searchQuery)}
${highlightMatch(result.description, searchQuery)}
${result.shortcut ? `${result.shortcut}` : ''}
`; } }); container.innerHTML = html; } /** * Highlight matching text in search results */ function highlightMatch(text, query) { if (!query || query.length < MIN_SEARCH_LENGTH) { return text; } const regex = new RegExp(`(${escapeRegex(query)})`, 'gi'); return text.replace(regex, '$1'); } /** * Escape regex special characters */ function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Format category title */ function formatCategoryTitle(category) { const titles = { 'recent': 'Recent', 'navigation': 'Navigation', 'fleet': 'Fleet Management', 'security': 'Security', 'nodes': 'Node Management', 'zones': 'Zones', 'view': 'View Controls', 'mode': 'Display Mode', 'theme': 'Theme', 'help': 'Help & Documentation' }; return titles[category] || category.charAt(0).toUpperCase() + category.slice(1); } // ============================================ // Command Execution // ============================================ /** * Execute a command */ async function executeCommand(commandId) { const command = commandRegistry.get(commandId); if (!command) { console.error('[Command Palette] Unknown command:', commandId); return; } console.log('[Command Palette] Executing:', commandId); // Add to recent commands addToRecent(commandId); // Close palette closePalette(); // Execute action try { await command.action(); } catch (error) { console.error('[Command Palette] Command error:', error); showToast(`Command failed: ${error.message}`, 'warning'); } } /** * Add command to recent list */ function addToRecent(commandId) { // Remove if already exists recentCommands = recentCommands.filter(id => id !== commandId); // Add to front recentCommands.unshift(commandId); // Trim to max if (recentCommands.length > MAX_RECENT) { recentCommands = recentCommands.slice(0, MAX_RECENT); } // Save to localStorage localStorage.setItem(STORAGE_KEY_RECENT, JSON.stringify(recentCommands)); } /** * Load recent commands from storage */ function loadRecentCommands() { try { const stored = localStorage.getItem(STORAGE_KEY_RECENT); if (stored) { recentCommands = JSON.parse(stored); } } catch (e) { console.error('[Command Palette] Error loading recent commands:', e); recentCommands = []; } } // ============================================ // Helper Functions // ============================================ /** * Navigate to a mode */ function navigateToMode(mode) { if (window.SpaxelRouter) { window.SpaxelRouter.navigate(mode); } else { window.location.hash = mode; } } /** * Confirm and execute an action */ async function confirmAndExecute(message, action) { const confirmed = confirm(message); if (confirmed) { await action(); } } /** * Show node selector */ async function showNodeSelector(action) { // Fetch nodes const response = await fetch('/api/nodes'); if (!response.ok) { showToast('Failed to load nodes', 'warning'); return; } const nodes = await response.json(); // Create selection UI const options = nodes.map(node => `` ).join(''); const selected = prompt(`Select node:\n${nodes.map((n, i) => `${i + 1}. ${n.name || n.mac}`).join('\n')}`); if (selected) { const node = nodes.find(n => n.mac === selected || n.name === selected); if (node) { executeNodeAction(action, node); } } } /** * Execute node action */ async function executeNodeAction(action, node) { switch (action) { case 'restart': await fetch(`/api/nodes/${node.mac}/reboot`, { method: 'POST' }); showToast(`Restarting ${node.name || node.mac}`, 'info'); break; case 'update': await fetch(`/api/nodes/${node.mac}/update`, { method: 'POST' }); showToast(`Updating ${node.name || node.mac}`, 'info'); break; } } /** * Show zone selector */ async function showZoneSelector(action) { // Fetch zones const response = await fetch('/api/zones'); if (!response.ok) { showToast('Failed to load zones', 'warning'); return; } const zones = await response.json(); const zoneNames = zones.map(z => z.name).join(', '); const selected = prompt(`Enter zone name:\nAvailable: ${zoneNames}`); if (selected) { const zone = zones.find(z => z.name.toLowerCase() === selected.toLowerCase()); if (zone) { // Open zone history or navigate to it if (window.SpaxelRouter) { window.SpaxelRouter.navigate('timeline'); } } } } /** * Show help topic */ function showHelpTopic(topic) { const helpContent = { 'fall-detection': { title: 'Fall Detection', content: `

How Fall Detection Works

Spaxel detects falls by monitoring rapid Z-axis movement followed by sustained stillness:

Requirements:

Reducing False Positives:

Fall detection is designed to distinguish falls from:

` }, 'accuracy': { title: 'Improving Accuracy', content: `

Tips for Better Detection

Node Placement:

Environment:

Calibration:

Advanced:

` }, 'false-positive': { title: 'Why Did This Happen?', content: `

Analyzing recent detection...

Possible causes:

What to do:

` }, 'shortcuts': { title: 'Keyboard Shortcuts', content: `

Global Shortcuts

3D View

Touch (Mobile)

Replay Mode

` } }; const help = helpContent[topic]; if (help) { // Show help modal showHelpModal(help.title, help.content); } else { showToast('Help topic not found', 'warning'); } } /** * Show help modal */ function showHelpModal(title, content) { const modal = document.createElement('div'); modal.className = 'command-help-modal visible'; modal.innerHTML = `

${title}

${content}
`; document.body.appendChild(modal); // Close handlers modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('help-backdrop') || e.target.classList.contains('help-close')) { modal.remove(); } }); } /** * Set theme */ function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('spaxel_theme', theme); showToast(`Switched to ${theme} mode`, 'info'); } /** * Show toast notification */ function showToast(message, type = 'info') { if (window.showToast) { window.showToast(message, type); return; } // Fallback toast const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: white; padding: 12px 20px; border-radius: 8px; z-index: 1000; `; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'fadeOut 0.3s ease-out forwards'; setTimeout(() => toast.remove(), 300); }, 3000); } // ============================================ // Initialization // ============================================ /** * Initialize the command palette */ function init() { console.log('[Command Palette] Initializing...'); // Load recent commands loadRecentCommands(); // Initialize default commands initializeCommands(); // Create UI (hidden) createCommandPalette(); // Show keyboard shortcut hint if not dismissed showShortcutHintIfNeeded(); console.log('[Command Palette] Initialized'); } /** * Show dismissible keyboard shortcut hint */ function showShortcutHintIfNeeded() { // Check if already dismissed if (localStorage.getItem(STORAGE_KEY_HINT_DISMISSED)) { return; } // Check if expert mode is active if (!isAvailable()) { return; } // Create hint element const hint = document.createElement('div'); hint.id = 'command-palette-shortcut-hint'; hint.className = 'command-shortcut-hint'; hint.innerHTML = ` Press Ctrl + K to open command palette `; // Add to body document.body.appendChild(hint); // Show after a short delay setTimeout(() => { hint.classList.add('visible'); }, 1000); // Handle dismiss hint.querySelector('.hint-dismiss').addEventListener('click', () => { hint.classList.remove('visible'); setTimeout(() => { hint.remove(); localStorage.setItem(STORAGE_KEY_HINT_DISMISSED, 'true'); }, 300); }); // Auto-hide after 10 seconds setTimeout(() => { if (hint.parentNode) { hint.classList.remove('visible'); setTimeout(() => { if (hint.parentNode) { hint.remove(); } }, 300); } }, 10000); } // ============================================ // Public API // ============================================ window.CommandPalette = { init: init, open: openPalette, close: closePalette, toggle: togglePalette, register: registerCommand, execute: executeCommand, isOpen: () => isOpen }; // Auto-initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } console.log('[Command Palette] Module loaded'); })();