Files
tyto/frontend/src/lib/components/Header.svelte
vikingowl 62219ea97a fix: add settings button to header and fix page title
- Add gear icon button to open settings/export panel (desktop + mobile)
- Fix page title from "System Monitor" to "Tyto" in +page.svelte

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 06:43:40 +01:00

243 lines
13 KiB
Svelte

<script lang="ts">
import { connected, systemInfo } from '$lib/stores/metrics';
import { settings } from '$lib/stores/settings';
import { theme } from '$lib/stores/theme';
import { showShortcutsHelp } from '$lib/stores/keyboard';
import { showSettings, editMode } from '$lib/stores/layout';
import { hosts } from '$lib/stores/hosts';
import { formatUptime } from '$lib/utils/formatters';
import HostSelector from './HostSelector.svelte';
const refreshRates = [1, 2, 5, 10, 30];
let mobileMenuOpen = $state(false);
// Show host selector if there are remote hosts configured
const showHostSelector = $derived($hosts.length > 1);
</script>
<header class="sticky top-0 z-50 backdrop-blur-xl border-b {$theme === 'light' ? 'border-black/5' : 'border-white/5'}">
<div class="absolute inset-0 {$theme === 'light' ? 'bg-gradient-to-r from-slate-100/90 via-white/90 to-slate-100/90' : 'bg-gradient-to-r from-slate-900/90 via-slate-800/90 to-slate-900/90'}"></div>
<div class="relative container mx-auto px-4 sm:px-6 py-3 sm:py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 sm:gap-6">
<!-- Logo -->
<div class="flex items-center gap-2 sm:gap-3">
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-gradient-to-br from-slate-700 to-slate-800 flex items-center justify-center shadow-lg shadow-slate-500/20 overflow-hidden">
<!-- Barn Owl (Tyto alba) icon -->
<svg class="w-7 h-7 sm:w-9 sm:h-9" viewBox="0 0 32 32" fill="none">
<!-- Heart-shaped face -->
<ellipse cx="16" cy="16" rx="10" ry="11" fill="#f8fafc"/>
<ellipse cx="16" cy="16" rx="8" ry="9" fill="#e2e8f0"/>
<!-- Eyes -->
<ellipse cx="12" cy="14" rx="3" ry="3.5" fill="#1e293b"/>
<ellipse cx="20" cy="14" rx="3" ry="3.5" fill="#1e293b"/>
<circle cx="12" cy="14" r="1.5" fill="#fbbf24"/>
<circle cx="20" cy="14" r="1.5" fill="#fbbf24"/>
<!-- Beak -->
<path d="M16 17 L14.5 19.5 L16 21 L17.5 19.5 Z" fill="#d97706"/>
</svg>
</div>
<div>
<h1 class="text-lg sm:text-xl font-bold bg-gradient-to-r {$theme === 'light' ? 'from-slate-800 to-slate-600' : 'from-white to-slate-300'} bg-clip-text text-transparent">
Tyto
</h1>
{#if $systemInfo}
<p class="text-[10px] sm:text-xs {$theme === 'light' ? 'text-slate-500' : 'text-slate-400'}">{$systemInfo.hostname}</p>
{/if}
</div>
</div>
<!-- System info badges - hidden on mobile -->
{#if $systemInfo}
<div class="hidden lg:flex items-center gap-2">
<span class="metric-badge">{$systemInfo.kernel}</span>
<span class="metric-badge">Up: {formatUptime($systemInfo.uptime)}</span>
</div>
{/if}
</div>
<!-- Desktop controls -->
<div class="hidden sm:flex items-center gap-3 sm:gap-4">
<!-- Host selector -->
<HostSelector />
<!-- Refresh rate -->
<div class="flex items-center gap-2">
<span class="text-xs {$theme === 'light' ? 'text-slate-500' : 'text-slate-400'} hidden md:inline">Refresh</span>
<select
class="{$theme === 'light' ? 'bg-black/5 border-black/10 text-slate-800' : 'bg-white/5 border-white/10 text-white'} border rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-blue-500/50 cursor-pointer"
value={$settings.refreshRate}
onchange={(e) => settings.setRefreshRate(parseInt(e.currentTarget.value))}
>
{#each refreshRates as rate}
<option value={rate} class="{$theme === 'light' ? 'bg-white' : 'bg-slate-800'}">{rate}s</option>
{/each}
</select>
</div>
<!-- Theme toggle -->
<button
onclick={() => theme.toggle()}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10' : 'bg-white/5 hover:bg-white/10'} p-2 rounded-lg transition-colors"
title="Toggle theme (T)"
>
{#if $theme === 'light'}
<svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" />
</svg>
{:else}
<svg class="w-5 h-5 text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
{/if}
</button>
<!-- Edit dashboard layout -->
<button
onclick={() => editMode.set(true)}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10 text-slate-500' : 'bg-white/5 hover:bg-white/10 text-slate-400'} p-2 rounded-lg transition-colors"
title="Edit dashboard layout"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg>
</button>
<!-- Settings / Export -->
<button
onclick={() => showSettings.set(true)}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10 text-slate-500' : 'bg-white/5 hover:bg-white/10 text-slate-400'} p-2 rounded-lg transition-colors"
title="Settings & Export (S)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<!-- Keyboard shortcuts help -->
<button
onclick={() => showShortcutsHelp.set(true)}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10 text-slate-500' : 'bg-white/5 hover:bg-white/10 text-slate-400'} p-2 rounded-lg transition-colors hidden md:block"
title="Keyboard shortcuts (?)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<!-- Connection status -->
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg {$connected ? 'bg-emerald-500/10' : 'bg-red-500/10'}">
<div
class="w-2 h-2 rounded-full {$connected ? 'bg-emerald-400 connected-indicator' : 'bg-red-400'}"
></div>
<span class="text-sm {$connected ? 'text-emerald-400' : 'text-red-400'}">
{$connected ? 'Live' : 'Offline'}
</span>
</div>
</div>
<!-- Mobile controls -->
<div class="flex sm:hidden items-center gap-2">
<!-- Connection status (compact) -->
<div class="w-2.5 h-2.5 rounded-full {$connected ? 'bg-emerald-400 connected-indicator' : 'bg-red-400'}"></div>
<!-- Mobile menu button -->
<button
onclick={() => mobileMenuOpen = !mobileMenuOpen}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10' : 'bg-white/5 hover:bg-white/10'} p-2 rounded-lg transition-colors"
aria-label="Toggle menu"
>
{#if mobileMenuOpen}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
{:else}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
{/if}
</button>
</div>
</div>
<!-- Mobile menu dropdown -->
{#if mobileMenuOpen}
<div class="sm:hidden mt-3 pt-3 border-t {$theme === 'light' ? 'border-black/10' : 'border-white/10'}">
<!-- System info -->
{#if $systemInfo}
<div class="flex flex-wrap gap-2 mb-3">
<span class="metric-badge text-xs">{$systemInfo.kernel}</span>
<span class="metric-badge text-xs">Up: {formatUptime($systemInfo.uptime)}</span>
</div>
{/if}
<div class="flex items-center justify-between gap-2">
<!-- Refresh rate -->
<div class="flex items-center gap-2 flex-1">
<span class="text-xs {$theme === 'light' ? 'text-slate-500' : 'text-slate-400'}">Refresh</span>
<select
class="{$theme === 'light' ? 'bg-black/5 border-black/10 text-slate-800' : 'bg-white/5 border-white/10 text-white'} border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 cursor-pointer flex-1"
value={$settings.refreshRate}
onchange={(e) => settings.setRefreshRate(parseInt(e.currentTarget.value))}
>
{#each refreshRates as rate}
<option value={rate} class="{$theme === 'light' ? 'bg-white' : 'bg-slate-800'}">{rate}s</option>
{/each}
</select>
</div>
<!-- Edit layout -->
<button
onclick={() => { editMode.set(true); mobileMenuOpen = false; }}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10 text-slate-500' : 'bg-white/5 hover:bg-white/10 text-slate-400'} p-2.5 rounded-lg transition-colors"
title="Edit layout"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg>
</button>
<!-- Settings / Export -->
<button
onclick={() => { showSettings.set(true); mobileMenuOpen = false; }}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10 text-slate-500' : 'bg-white/5 hover:bg-white/10 text-slate-400'} p-2.5 rounded-lg transition-colors"
title="Settings & Export"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<!-- Theme toggle -->
<button
onclick={() => theme.toggle()}
class="{$theme === 'light' ? 'bg-black/5 hover:bg-black/10' : 'bg-white/5 hover:bg-white/10'} p-2.5 rounded-lg transition-colors"
title="Toggle theme"
>
{#if $theme === 'light'}
<svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" />
</svg>
{:else}
<svg class="w-5 h-5 text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
{/if}
</button>
<!-- Connection status badge -->
<div class="flex items-center gap-2 px-3 py-2 rounded-lg {$connected ? 'bg-emerald-500/10' : 'bg-red-500/10'}">
<div class="w-2 h-2 rounded-full {$connected ? 'bg-emerald-400' : 'bg-red-400'}"></div>
<span class="text-sm {$connected ? 'text-emerald-400' : 'text-red-400'}">
{$connected ? 'Live' : 'Offline'}
</span>
</div>
</div>
</div>
{/if}
</div>
</header>