initial commit
Generate a build and push to Cloudflare Pages / Build and Deploy to Cloudflare Pages (push) Successful in 1m24s

This commit is contained in:
2026-03-25 18:53:21 -04:00
parent 2d19da4ef5
commit d21806dfd1
532 changed files with 11274 additions and 72849 deletions
+246
View File
@@ -0,0 +1,246 @@
<script lang="ts">
import { onMount } from 'svelte'
import { slide, fade } from 'svelte/transition'
import { cn } from '@/lib/utils'
import { NAV_LINKS, SITE } from '@/consts'
import Logo from './Logo.svelte'
import Link from './Link.svelte'
import ThemeToggle from './ThemeToggle.svelte'
// Icons from unplugin-icons (bundled at build time, no flash)
import IconDownload from '~icons/lucide/download'
import IconMenu from '~icons/lucide/menu'
import IconX from '~icons/lucide/x'
let scrollLevel = $state(0)
let isScrolled = $state(false)
let isMobile = $state(false)
let mobileMenuOpen = $state(false)
let activePath = $state('/')
// Width transitions from padded full-width down to content width (56rem)
// Using max() to ensure width never goes below 56rem at any scroll level
const widthMap: Record<number, string> = {
0: 'calc(100% - 3rem)', // Padded from edges at top
1: 'max(calc(100% - 4rem), 56rem)',
2: 'max(calc(100% - 6rem), 56rem)',
3: 'max(calc(100% - 8rem), 56rem)',
4: '56rem', // Matches actual content width
}
let headerWidth = $derived(isMobile ? '100%' : widthMap[scrollLevel])
onMount(() => {
activePath = window.location.pathname
const handleRouteChange = () => {
activePath = window.location.pathname
}
let resizeTimer: ReturnType<typeof setTimeout>
const handleResize = () => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
const isMobileView = window.matchMedia('(max-width: 768px)').matches
isMobile = isMobileView
if (!isMobileView && mobileMenuOpen) {
mobileMenuOpen = false
}
}, 100)
}
let scrollTimer: ReturnType<typeof setTimeout>
const handleScroll = () => {
clearTimeout(scrollTimer)
scrollTimer = setTimeout(() => {
const scrollY = window.scrollY
scrollLevel = scrollY > 500 ? 4 : scrollY > 300 ? 3 : scrollY > 150 ? 2 : scrollY > 0 ? 1 : 0
isScrolled = scrollY > 0
}, 50)
}
handleResize()
handleScroll()
window.addEventListener('popstate', handleRouteChange)
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('popstate', handleRouteChange)
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
clearTimeout(resizeTimer)
clearTimeout(scrollTimer)
}
})
$effect(() => {
if (typeof document !== 'undefined') {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
})
function toggleMobileMenu() {
mobileMenuOpen = !mobileMenuOpen
}
function closeMobileMenu() {
mobileMenuOpen = false
}
function handleNavClick(href: string) {
activePath = href
}
</script>
<header
aria-label="Navigation"
role="banner"
style="width: {headerWidth};"
class={cn(
'fixed left-1/2 z-30 -translate-x-1/2 transform backdrop-blur-lg',
'bg-background/80 border-0',
'rounded-none shadow-none transition-all duration-300 ease-in-out',
'border border-transparent',
isScrolled && !isMobile && 'rounded-full',
isScrolled && !isMobile && 'backdrop-blur-md',
isScrolled && !isMobile && 'border-foreground/10',
isScrolled && !isMobile && 'border',
isScrolled && !isMobile && 'bg-background/80',
isScrolled && !isMobile && 'max-w-[min(90vw,calc(100vw-2rem))]',
!isMobile && 'top-2 lg:top-4 xl:top-6',
isMobile && 'top-0',
isMobile && 'rounded-none',
isMobile && 'border-0',
isMobile && 'shadow-none'
)}
>
<div class="mx-auto flex max-w-7xl items-center justify-between gap-4 p-4">
<a
href="/"
class="font-custom flex shrink-0 items-center gap-2 text-xl font-bold"
aria-label="Home"
title="Home"
>
<Logo class="h-8 w-8 mr-2" />
<span class="transition-opacity duration-200 ease-in-out text-foreground/90 dark:text-white">
{SITE.title}
</span>
</a>
<div class="flex items-center gap-2 md:gap-4">
<nav class="hidden items-center gap-6 md:flex" aria-label="Main navigation">
{#each NAV_LINKS as item}
{@const isActive = activePath.startsWith(item.href) && item.href !== '/'}
<div class="relative hover:scale-105 transition-transform">
<a
href={item.href}
class={cn(
'text-sm font-medium capitalize transition-colors duration-200',
'relative py-1 px-1',
'after:absolute after:bottom-0 after:left-0 after:h-[2px] after:w-0 after:bg-primary after:transition-all after:duration-300',
'hover:after:w-full hover:text-foreground',
isActive
? 'text-foreground after:w-full after:bg-primary'
: 'text-foreground/70'
)}
onclick={() => handleNavClick(item.href)}
>
{item.label}
</a>
</div>
{/each}
<a
href="/static/resume.pdf"
download
class="flex items-center gap-2 rounded-full border border-foreground/20 px-4 py-1.5 text-sm font-medium transition-all duration-200 hover:border-foreground/40 hover:bg-foreground/5"
>
Resume
<IconDownload class="size-4" />
</a>
</nav>
<ThemeToggle />
{#if isMobile}
<button
onclick={toggleMobileMenu}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
class="ml-1 h-9 w-9 rounded-full p-0 transition-colors duration-200 ease-in-out inline-flex items-center justify-center hover:bg-accent hover:text-accent-foreground cursor-pointer"
>
{#if mobileMenuOpen}
<IconX class="h-5 w-5" />
{:else}
<IconMenu class="h-5 w-5" />
{/if}
</button>
{/if}
</div>
</div>
</header>
{#if mobileMenuOpen}
<div
class="fixed inset-0 z-20 flex flex-col items-center justify-start bg-background border-0 shadow-none"
transition:fade={{ duration: 200 }}
>
<div class="flex flex-col items-center justify-start h-full pt-24 w-full p-6">
<nav class="flex flex-col items-center justify-start gap-1 w-full">
{#each NAV_LINKS as item, i}
<div
class="w-full text-start"
transition:slide={{ delay: i * 50, duration: 200 }}
>
<a
href={item.href}
onclick={closeMobileMenu}
class="dark:text-white text-lg font-bold font-custom capitalize dark:hover:text-white/80 transition-colors inline-block py-2 relative group"
>
{item.label}
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-neutral-900 dark:bg-white group-hover:w-full transition-all duration-300 ease-in-out"></span>
</a>
</div>
{/each}
<div
class="w-full text-start mt-4"
transition:slide={{ delay: NAV_LINKS.length * 50, duration: 200 }}
>
<a
href="/static/resume.pdf"
download
onclick={closeMobileMenu}
class="inline-flex items-center gap-2 rounded-full border border-foreground/20 px-5 py-2 text-lg font-bold font-custom transition-all duration-200 hover:border-foreground/40 hover:bg-foreground/5"
>
Resume
<IconDownload class="size-5" />
</a>
</div>
</nav>
<div class="mt-auto flex flex-col items-center gap-6">
<div class="flex flex-wrap items-center justify-center gap-x-2 text-center">
<span class="text-muted-foreground text-sm" aria-label="copyright">
2025 - {new Date().getFullYear()} &copy; All rights reserved.
</span>
<div class="hidden h-4 w-px bg-border sm:block"></div>
<p class="text-muted-foreground text-sm" aria-label="open-source description">
<Link
href="https://git.jaroszew.ski/patrick/portfolio"
class="text-foreground"
external
underline
>
Open-source
</Link>
under MIT license
</p>
</div>
</div>
</div>
</div>
{/if}