- 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>
243 lines
13 KiB
Svelte
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>
|