aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/server/static/style.css133
-rw-r--r--internal/server/templates/index.html196
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>