diff options
| author | Fuwn <[email protected]> | 2026-01-20 05:42:29 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 05:42:29 -0800 |
| commit | 417b707e8ecedd8e55c35a86d99c85a2b0239663 (patch) | |
| tree | 49b2bce351f5f04aa8e7e7647863044ffcc8e07d | |
| parent | fix: Hide ping in tooltips when hide_ping is enabled (diff) | |
| download | kaze-417b707e8ecedd8e55c35a86d99c85a2b0239663.tar.xz kaze-417b707e8ecedd8e55c35a86d99c85a2b0239663.zip | |
feat: Add command palette for quick navigation
| -rw-r--r-- | internal/server/static/style.css | 133 | ||||
| -rw-r--r-- | internal/server/templates/index.html | 196 |
2 files changed, 328 insertions, 1 deletions
diff --git a/internal/server/static/style.css b/internal/server/static/style.css index 2e57537..ebff1dc 100644 --- a/internal/server/static/style.css +++ b/internal/server/static/style.css @@ -402,3 +402,136 @@ svg { fill: none; } [data-tooltip] { cursor: help; } + +/* Command Palette */ +.command-palette { + display: none; + position: fixed; + inset: 0; + z-index: 2000; +} + +.command-palette.visible { + display: block; +} + +.command-palette-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); +} + +.command-palette-container { + position: absolute; + top: 20%; + left: 50%; + transform: translateX(-50%); + width: 90%; + max-width: 32rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + overflow: hidden; +} + +.command-input { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-secondary); + border: none; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + font-family: inherit; + font-size: 0.875rem; + outline: none; +} + +.command-input::placeholder { + color: var(--text-tertiary); +} + +.command-results { + max-height: 20rem; + overflow-y: auto; +} + +.command-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 1rem; + cursor: pointer; + border-bottom: 1px solid var(--border-color); +} + +.command-item:last-child { + border-bottom: none; +} + +.command-item:hover, +.command-item.selected { + background: var(--bg-tertiary); +} + +.command-item-icon { + width: 1rem; + height: 1rem; + color: var(--text-tertiary); + flex-shrink: 0; +} + +.command-item-content { + flex: 1; + min-width: 0; +} + +.command-item-name { + color: var(--text-primary); + font-size: 0.75rem; +} + +.command-item-type { + color: var(--text-tertiary); + font-size: 0.6875rem; + text-transform: uppercase; +} + +.command-item-path { + color: var(--text-tertiary); + font-size: 0.6875rem; +} + +.command-hint { + display: flex; + gap: 1rem; + padding: 0.5rem 1rem; + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); + font-size: 0.6875rem; + color: var(--text-tertiary); +} + +.command-hint kbd { + display: inline-block; + padding: 0.1rem 0.3rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + font-family: inherit; + font-size: 0.625rem; +} + +.command-empty { + padding: 1.5rem 1rem; + text-align: center; + color: var(--text-tertiary); + font-size: 0.75rem; +} + +/* Highlight for jumped-to items */ +.highlight-jump { + animation: highlight-pulse 1s ease-out; +} + +@keyframes highlight-pulse { + 0% { background-color: var(--bg-tertiary); } + 100% { background-color: transparent; } +} diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html index d689416..b5a2462 100644 --- a/internal/server/templates/index.html +++ b/internal/server/templates/index.html @@ -78,7 +78,7 @@ <div class="divide-y divide-neutral-200 dark:divide-neutral-800 group-content" data-group-content="{{$group.Name}}" data-default-collapsed="{{$group.DefaultCollapsed}}"> {{range .Monitors}} {{$monitor := .}} - <div class="p-4 hover:bg-neutral-100/50 dark:hover:bg-neutral-900/50 transition-colors"> + <div class="p-4 hover:bg-neutral-100/50 dark:hover:bg-neutral-900/50 transition-colors" data-monitor="{{.Name}}" data-group="{{$group.Name}}"> <div class="flex items-start justify-between gap-4"> <div class="flex-1 min-w-0"> <div class="flex items-center gap-2 mb-2"> @@ -196,6 +196,20 @@ <!-- Tooltip container --> <div id="tooltip" class="tooltip"></div> + <!-- Command Palette --> + <div id="command-palette" class="command-palette"> + <div class="command-palette-backdrop"></div> + <div class="command-palette-container"> + <input type="text" id="command-input" class="command-input" placeholder="Search groups and monitors..." autocomplete="off" /> + <div id="command-results" class="command-results"></div> + <div class="command-hint"> + <span><kbd>↑↓</kbd> navigate</span> + <span><kbd>↵</kbd> select</span> + <span><kbd>esc</kbd> close</span> + </div> + </div> + </div> + <script> // Group collapse/expand functionality function toggleGroup(groupName) { @@ -486,6 +500,186 @@ }); })(); {{end}} + + // Command Palette + (function() { + const palette = document.getElementById('command-palette'); + const input = document.getElementById('command-input'); + const results = document.getElementById('command-results'); + let selectedIndex = -1; + let items = []; + + // Build search index + function buildIndex() { + const index = []; + + // Add groups + document.querySelectorAll('[data-group-content]').forEach(function(el) { + const name = el.getAttribute('data-group-content'); + index.push({ + type: 'group', + name: name, + element: el.closest('section'), + searchText: name.toLowerCase() + }); + }); + + // Add monitors + document.querySelectorAll('[data-monitor]').forEach(function(el) { + const name = el.getAttribute('data-monitor'); + const group = el.getAttribute('data-group'); + index.push({ + type: 'monitor', + name: name, + group: group, + element: el, + searchText: (name + ' ' + group).toLowerCase() + }); + }); + + return index; + } + + const searchIndex = buildIndex(); + + function openPalette() { + palette.classList.add('visible'); + input.value = ''; + input.focus(); + search(''); + } + + function closePalette() { + palette.classList.remove('visible'); + selectedIndex = -1; + } + + function search(query) { + const q = query.toLowerCase().trim(); + + if (q === '') { + items = searchIndex.slice(0, 10); + } else { + items = searchIndex.filter(function(item) { + return item.searchText.includes(q); + }).slice(0, 10); + } + + renderResults(); + } + + function renderResults() { + if (items.length === 0) { + results.innerHTML = '<div class="command-empty">No results found</div>'; + return; + } + + results.innerHTML = items.map(function(item, i) { + const isSelected = i === selectedIndex ? ' selected' : ''; + const icon = item.type === 'group' + ? '<svg class="command-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>' + : '<svg class="command-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" stroke-width="2"/></svg>'; + + const meta = item.type === 'group' + ? '<span class="command-item-type">Group</span>' + : '<span class="command-item-path">' + item.group + '</span>'; + + return '<div class="command-item' + isSelected + '" data-index="' + i + '">' + + icon + + '<div class="command-item-content">' + + '<div class="command-item-name">' + item.name + '</div>' + + meta + + '</div></div>'; + }).join(''); + } + + function selectItem(index) { + if (index < 0 || index >= items.length) return; + + const item = items[index]; + closePalette(); + + // Expand group if collapsed + if (item.type === 'group') { + const groupName = item.name; + const content = document.querySelector('[data-group-content="' + groupName + '"]'); + const icon = document.querySelector('[data-group-icon="' + groupName + '"]'); + + if (content && content.classList.contains('collapsed')) { + content.classList.remove('collapsed'); + if (icon) icon.classList.remove('rotated'); + localStorage.setItem('group-' + groupName, 'expanded'); + } + } else if (item.type === 'monitor') { + // Expand parent group if collapsed + const groupName = item.group; + const content = document.querySelector('[data-group-content="' + groupName + '"]'); + const icon = document.querySelector('[data-group-icon="' + groupName + '"]'); + + if (content && content.classList.contains('collapsed')) { + content.classList.remove('collapsed'); + if (icon) icon.classList.remove('rotated'); + localStorage.setItem('group-' + groupName, 'expanded'); + } + } + + // Scroll to element and highlight + setTimeout(function() { + item.element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + item.element.classList.add('highlight-jump'); + setTimeout(function() { + item.element.classList.remove('highlight-jump'); + }, 1000); + }, 50); + } + + // Event listeners + document.addEventListener('keydown', function(e) { + // Open with Cmd/Ctrl+K or / + if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || (e.key === '/' && !palette.classList.contains('visible') && document.activeElement.tagName !== 'INPUT')) { + e.preventDefault(); + openPalette(); + return; + } + + if (!palette.classList.contains('visible')) return; + + if (e.key === 'Escape') { + e.preventDefault(); + closePalette(); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, items.length - 1); + renderResults(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, -1); + renderResults(); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (selectedIndex >= 0) { + selectItem(selectedIndex); + } else if (items.length > 0) { + selectItem(0); + } + } + }); + + input.addEventListener('input', function() { + selectedIndex = -1; + search(input.value); + }); + + results.addEventListener('click', function(e) { + const item = e.target.closest('.command-item'); + if (item) { + selectItem(parseInt(item.getAttribute('data-index'), 10)); + } + }); + + // Close on backdrop click + palette.querySelector('.command-palette-backdrop').addEventListener('click', closePalette); + })(); </script> </body> </html> |