173 lines
5.1 KiB
Svelte
173 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
import { resolve } from '$app/paths';
|
|
import { page } from '$app/stores';
|
|
|
|
interface Props {
|
|
collapsed?: boolean;
|
|
onToggle?: () => void;
|
|
onMobileClose?: () => void;
|
|
}
|
|
|
|
let { collapsed = false, onToggle, onMobileClose }: Props = $props();
|
|
|
|
const links = [
|
|
{
|
|
title: '7-Day Forecast',
|
|
url: '/weather/week' as const,
|
|
iconPaths: [
|
|
'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'
|
|
]
|
|
},
|
|
{
|
|
title: 'Model Comparison',
|
|
url: '/weather/compare' as const,
|
|
iconPaths: ['M13 7h8m0 0v8m0-8l-8 8-4-4-6 6']
|
|
},
|
|
{
|
|
title: '14-Day Forecast',
|
|
url: '/weather/14-day' as const,
|
|
iconPaths: [
|
|
'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'
|
|
]
|
|
},
|
|
{
|
|
title: 'Maps',
|
|
url: '/weather/maps' as const,
|
|
// Heroicons "map" outline icon
|
|
iconPaths: [
|
|
'M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7'
|
|
]
|
|
}
|
|
];
|
|
|
|
let currentPath = $derived($page.url.pathname);
|
|
|
|
const isActive = (url: string) => {
|
|
return currentPath === url || currentPath.startsWith(url + '/');
|
|
};
|
|
</script>
|
|
|
|
<aside
|
|
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200"
|
|
class:w-55={!collapsed}
|
|
class:w-14={collapsed}
|
|
>
|
|
<!-- Sidebar header: same height as the topbar so the borders align; the
|
|
home link fills the entire row, padding included -->
|
|
<div class="flex h-14 shrink-0 items-stretch border-b border-sidebar-border">
|
|
<a
|
|
href={resolve('/weather/week')}
|
|
class="flex flex-1 items-center gap-2.5 transition-colors hover:bg-sidebar-accent {collapsed
|
|
? 'justify-center'
|
|
: 'px-4'}"
|
|
onclick={onMobileClose}
|
|
aria-label="Drizzli home"
|
|
>
|
|
<div
|
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground"
|
|
>
|
|
<!-- umbrella-with-rain logo mark (matches favicon) -->
|
|
<svg
|
|
class="h-5 w-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.75"
|
|
>
|
|
<!-- rain falls from above onto the canopy -->
|
|
<path stroke-linecap="round" d="M4.5 3v.01M19.5 3v.01M8.5 1.5v.01M15.5 2.5v.01" />
|
|
<path stroke-linecap="round" d="M12 4.5V6" />
|
|
<path
|
|
fill="currentColor"
|
|
stroke="none"
|
|
d="M4 14a8 8 0 0 1 16 0c-.66-1-1.99-1-2.66 0-.67-1-2-1-2.67 0-.67-1-2-1-2.67 0-.67-1-2-1-2.67 0C8.66 13 7.33 13 6.66 14 6 13 4.66 13 4 14Z"
|
|
/>
|
|
<path stroke-linecap="round" d="M12 14v5a1.9 1.9 0 0 1-3.8 0" />
|
|
</svg>
|
|
</div>
|
|
{#if !collapsed}
|
|
<span class="text-sm font-bold tracking-tight whitespace-nowrap text-sidebar-foreground">
|
|
Drizz.li
|
|
</span>
|
|
{/if}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Navigation links -->
|
|
<nav class="flex-1 space-y-1 px-2 py-3">
|
|
{#each links as link (link.title)}
|
|
{@const active = isActive(link.url)}
|
|
<a
|
|
href={resolve(link.url)}
|
|
class="relative flex items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100 {active
|
|
? 'bg-sidebar-accent text-sidebar-primary! opacity-100! font-semibold! nav-active'
|
|
: ''}"
|
|
title={collapsed ? link.title : undefined}
|
|
onclick={onMobileClose}
|
|
>
|
|
<div class="flex h-5 w-5 shrink-0 items-center justify-center">
|
|
<svg
|
|
class="h-4.5 w-4.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.75"
|
|
>
|
|
{#each link.iconPaths as d (d)}
|
|
<path stroke-linecap="round" stroke-linejoin="round" {d} />
|
|
{/each}
|
|
</svg>
|
|
</div>
|
|
{#if !collapsed}
|
|
<span class="ml-2.5 whitespace-nowrap">{link.title}</span>
|
|
{/if}
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
|
|
<!-- Collapse toggle (desktop sidebar only; the mobile drawer omits onToggle) -->
|
|
{#if onToggle}
|
|
<div class="border-t border-sidebar-border px-2 py-3">
|
|
<button
|
|
class="relative flex w-full cursor-pointer items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100"
|
|
onclick={onToggle}
|
|
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
<div class="flex h-5 w-5 shrink-0 items-center justify-center">
|
|
<svg
|
|
class="h-4.5 w-4.5 transition-transform duration-200"
|
|
class:rotate-180={collapsed}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.75"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
{#if !collapsed}
|
|
<span class="ml-2.5 whitespace-nowrap">Collapse</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</aside>
|
|
|
|
<style>
|
|
.nav-active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3px;
|
|
height: 60%;
|
|
border-radius: 0 3px 3px 0;
|
|
background: var(--sidebar-primary);
|
|
}
|
|
</style>
|