315 lines
8.7 KiB
Svelte
315 lines
8.7 KiB
Svelte
<script lang="ts">
|
|
import { mode, colorTheme, toggleMode, setColorTheme, themeOptions, themeColors, type ColorTheme } from '$lib/stores/theme';
|
|
import { page } from '$app/stores';
|
|
import { fly, fade, slide } from 'svelte/transition';
|
|
import { user, navigation, colorPalette } from '$lib/config';
|
|
import Icon from '@iconify/svelte';
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import '$lib/assets/css/navbar-waybar.css';
|
|
|
|
// State
|
|
let themeDropdownOpen = $state(false);
|
|
let mobileMenuOpen = $state(false);
|
|
let currentTime = $state(new Date());
|
|
|
|
// Derived values (Svelte 5 runes)
|
|
let currentPath = $derived($page.url.pathname);
|
|
|
|
// Time update interval
|
|
let timeInterval: ReturnType<typeof setInterval>;
|
|
|
|
onMount(() => {
|
|
// Update time every second
|
|
timeInterval = setInterval(() => {
|
|
currentTime = new Date();
|
|
}, 1000);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (timeInterval) clearInterval(timeInterval);
|
|
});
|
|
|
|
// Format time
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false
|
|
});
|
|
}
|
|
|
|
function formatDate(date: Date): string {
|
|
return date.toLocaleDateString('en-US', {
|
|
weekday: 'short',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
// Get current workspace/page index
|
|
function getWorkspaceIndex(path: string): number {
|
|
const idx = navigation.findIndex(n => n.path === path);
|
|
return idx >= 0 ? idx + 1 : 1;
|
|
}
|
|
|
|
// Battery icon based on level
|
|
function getBatteryIcon(level: number, charging: boolean): string {
|
|
if (charging) return 'mdi:battery-charging';
|
|
if (level > 90) return 'mdi:battery';
|
|
if (level > 70) return 'mdi:battery-80';
|
|
if (level > 50) return 'mdi:battery-60';
|
|
if (level > 30) return 'mdi:battery-40';
|
|
if (level > 10) return 'mdi:battery-20';
|
|
return 'mdi:battery-alert';
|
|
}
|
|
|
|
function handleThemeSelect(theme: ColorTheme) {
|
|
setColorTheme(theme);
|
|
themeDropdownOpen = false;
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
themeDropdownOpen = false;
|
|
mobileMenuOpen = false;
|
|
}
|
|
}
|
|
|
|
function closeMobileMenu() {
|
|
mobileMenuOpen = false;
|
|
}
|
|
|
|
function getThemeIcon(theme: ColorTheme): string {
|
|
switch (theme) {
|
|
case 'arch': return 'mdi:arch';
|
|
case 'catppuccin': return 'solar:cat-bold';
|
|
default: return 'mdi:palette';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window on:keydown={handleKeydown} />
|
|
|
|
<nav
|
|
class="waybar"
|
|
style="
|
|
--bar-bg: {$themeColors.background};
|
|
--bar-bg-module: {$themeColors.backgroundLight};
|
|
--bar-border: {$themeColors.border};
|
|
--bar-text: {$themeColors.text};
|
|
--bar-primary: {$themeColors.primary};
|
|
--bar-accent: {$themeColors.accent};
|
|
--bar-muted: {$themeColors.textMuted};
|
|
--bar-success: {colorPalette.success};
|
|
--bar-warning: {colorPalette.warning};
|
|
--bar-error: {colorPalette.error};
|
|
"
|
|
>
|
|
<!-- Left modules -->
|
|
<div class="bar-left">
|
|
<!-- Arch logo / Launcher -->
|
|
<a href="/" class="module launcher" title="Home">
|
|
<img src="/favicon.png" alt="Blob Icon" width="16" />
|
|
</a>
|
|
|
|
<!-- Mobile menu toggle -->
|
|
<button
|
|
class="mobile-menu-toggle"
|
|
onclick={() => mobileMenuOpen = !mobileMenuOpen}
|
|
aria-expanded={mobileMenuOpen}
|
|
aria-label="Toggle navigation menu"
|
|
>
|
|
<Icon icon={mobileMenuOpen ? 'mdi:close' : 'mdi:menu'} width="20" />
|
|
</button>
|
|
|
|
<!-- Workspaces (desktop) -->
|
|
<div class="module workspaces desktop-only">
|
|
{#each navigation as nav}
|
|
{@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
|
|
{#if nav.external}
|
|
<a
|
|
href={nav.path}
|
|
class="workspace external"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
title={nav.name}
|
|
>
|
|
<Icon icon="mdi:open-in-new" width="12" />
|
|
<span class="ws-name">{nav.name}</span>
|
|
</a>
|
|
{:else}
|
|
<a
|
|
href={nav.path}
|
|
class="workspace"
|
|
class:active={isActive}
|
|
title={nav.name}
|
|
>
|
|
<span class="ws-name">{nav.name}</span>
|
|
</a>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Current window title (desktop) -->
|
|
<div class="module window-title desktop-only">
|
|
<span class="title-icon">
|
|
{#if currentPath === '/'}
|
|
<Icon icon="mdi:home" width="14" />
|
|
{:else if currentPath === '/portfolio'}
|
|
<Icon icon="mdi:folder-multiple" width="14" />
|
|
{:else if currentPath === '/models'}
|
|
<Icon icon="mdi:cube-outline" width="14" />
|
|
{:else if currentPath === '/projects'}
|
|
<Icon icon="mdi:trophy" width="14" />
|
|
{:else}
|
|
<Icon icon="mdi:file" width="14" />
|
|
{/if}
|
|
</span>
|
|
<span class="title-text">
|
|
{navigation.find(n => n.path === currentPath)?.name || 'page'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center modules -->
|
|
<div class="bar-center">
|
|
<div class="module clock">
|
|
<Icon icon="mdi:clock-outline" width="14" />
|
|
<span class="time">{formatTime(currentTime)}</span>
|
|
<span class="date">{formatDate(currentTime)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right modules -->
|
|
<div class="bar-right">
|
|
<!-- (No system modules — minimal Waybar look) -->
|
|
|
|
<!-- Theme selector -->
|
|
<div class="module theme-selector">
|
|
<button
|
|
class="theme-trigger"
|
|
onclick={() => themeDropdownOpen = !themeDropdownOpen}
|
|
aria-expanded={themeDropdownOpen}
|
|
title="Theme: {$colorTheme}"
|
|
>
|
|
<Icon icon={getThemeIcon($colorTheme)} width="16" />
|
|
</button>
|
|
|
|
{#if themeDropdownOpen}
|
|
<div
|
|
class="theme-dropdown"
|
|
transition:fly={{ y: -10, duration: 150 }}
|
|
>
|
|
<div class="dropdown-header">Theme</div>
|
|
{#each themeOptions as option}
|
|
<button
|
|
class="theme-option"
|
|
class:active={$colorTheme === option.value}
|
|
onclick={() => handleThemeSelect(option.value)}
|
|
>
|
|
<Icon icon={getThemeIcon(option.value)} width="16" />
|
|
<span>{option.label}</span>
|
|
{#if $colorTheme === option.value}
|
|
<Icon icon="mdi:check" width="14" class="check" />
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
<div class="dropdown-divider"></div>
|
|
<div class="dropdown-header">Mode</div>
|
|
<button class="theme-option" onclick={toggleMode}>
|
|
<Icon icon={$mode === 'dark' ? 'mdi:weather-sunny' : 'mdi:weather-night'} width="16" />
|
|
<span>{$mode === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backdrop -->
|
|
{#if themeDropdownOpen || mobileMenuOpen}
|
|
<button
|
|
class="backdrop"
|
|
transition:fade={{ duration: 100 }}
|
|
onclick={() => { themeDropdownOpen = false; mobileMenuOpen = false; }}
|
|
aria-label="Close"
|
|
></button>
|
|
{/if}
|
|
</nav>
|
|
|
|
<!-- Mobile menu dropdown -->
|
|
{#if mobileMenuOpen}
|
|
<div
|
|
class="mobile-menu"
|
|
transition:slide={{ duration: 200 }}
|
|
style="
|
|
--bar-bg: {$themeColors.background};
|
|
--bar-bg-module: {$themeColors.backgroundLight};
|
|
--bar-border: {$themeColors.border};
|
|
--bar-text: {$themeColors.text};
|
|
--bar-primary: {$themeColors.primary};
|
|
--bar-accent: {$themeColors.accent};
|
|
--bar-muted: {$themeColors.textMuted};
|
|
"
|
|
>
|
|
<div class="mobile-nav-links">
|
|
{#each navigation as nav}
|
|
{@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
|
|
{#if nav.external}
|
|
<a
|
|
href={nav.path}
|
|
class="mobile-nav-link external"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onclick={closeMobileMenu}
|
|
>
|
|
<Icon icon="mdi:open-in-new" width="16" />
|
|
<span>{nav.name}</span>
|
|
</a>
|
|
{:else}
|
|
<a
|
|
href={nav.path}
|
|
class="mobile-nav-link"
|
|
class:active={isActive}
|
|
onclick={closeMobileMenu}
|
|
>
|
|
{#if nav.path === '/'}
|
|
<Icon icon="mdi:home" width="16" />
|
|
{:else if nav.path === '/portfolio'}
|
|
<Icon icon="mdi:folder-multiple" width="16" />
|
|
{:else if nav.path === '/models'}
|
|
<Icon icon="mdi:cube-outline" width="16" />
|
|
{:else if nav.path === '/projects'}
|
|
<Icon icon="mdi:trophy" width="16" />
|
|
{:else}
|
|
<Icon icon="mdi:file" width="16" />
|
|
{/if}
|
|
<span>{nav.name}</span>
|
|
</a>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="mobile-menu-divider"></div>
|
|
|
|
<div class="mobile-theme-section">
|
|
<div class="mobile-section-header">Theme</div>
|
|
<div class="mobile-theme-options">
|
|
{#each themeOptions as option}
|
|
<button
|
|
class="mobile-theme-btn"
|
|
class:active={$colorTheme === option.value}
|
|
onclick={() => { handleThemeSelect(option.value); closeMobileMenu(); }}
|
|
>
|
|
<Icon icon={getThemeIcon(option.value)} width="18" />
|
|
<span>{option.label}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<button class="mobile-mode-btn" onclick={() => { toggleMode(); closeMobileMenu(); }}>
|
|
<Icon icon={$mode === 'dark' ? 'mdi:weather-sunny' : 'mdi:weather-night'} width="18" />
|
|
<span>{$mode === 'dark' ? 'Switch to Light' : 'Switch to Dark'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|