Files
landing/src/lib/components/Navigation.svelte
mifi 9e692d072b
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
Prettier
2026-03-09 20:28:12 -03:00

408 lines
11 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { page as pageState } from '$app/state';
import Wordmark from './Wordmark.svelte';
const path = $derived(pageState.url?.pathname ?? '/');
/** Page slug for body class: "page-home" | "page-services" | "page-services-hands-on-saas-architecture-consultant" etc. Set at build time per route; no client JS. */
const bodyClass = $derived(
path === '/'
? 'page-home'
: 'page-' + path.replace(/^\/|\/$/g, '').replace(/\//g, '-'),
);
interface NavigationItem {
label: string;
href: string;
umamiEventLabel: string;
}
const { items = [], page }: { items: NavigationItem[]; page: string } = $props();
</script>
<nav id="nav" class="nav" aria-label="Main navigation">
<input
type="checkbox"
id="nav-toggle"
class="nav-toggle-input"
aria-hidden="true"
hidden
/>
<div class="mobile-nav-header">
<span
class={['mobile nav-header-logo', { 'page-home': bodyClass === 'page-home' }]}
>
<Wordmark />
</span>
<button
type="button"
id="nav-toggle-button"
class="btn-clear"
aria-controls="nav-menu"
aria-label="Toggle navigation"
aria-expanded="false"
>
<label
id="nav-toggle-label"
for="nav-toggle"
class="nav-toggle"
role="presentation"
>
<span class="nav-toggle-inner">
<span class="nav-toggle-line"></span>
<span class="nav-toggle-line"></span>
<span class="nav-toggle-line"></span>
</span>
</label>
</button>
</div>
<div id="nav-menu" class="nav-menu container">
<span
class={[
'nav-header-logo desktop',
{ 'page-home': bodyClass === 'page-home' },
]}
>
{#if page !== 'home'}
<a href="/">
<Wordmark />
<span class="sr-only">mifi Ventures home page</span>
</a>
{:else}
<Wordmark />
{/if}
</span>
<ul class="nav-list">
{#each items as item (item.href)}
<li class="nav-item">
<a
href={item.href}
class="nav-link"
data-umami-event="navigation"
data-umami-event-label={item.umamiEventLabel}
data-umami-event-page={page}
>
{item.label}
</a>
</li>
{/each}
</ul>
<div class="nav-item nav-back-to-top">
<a
href="#header"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="back to top">Back to top</a
>
</div>
</div>
</nav>
<style>
.nav {
background-color: var(--color-bg);
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-border);
padding-top: var(--space-sm);
position: sticky;
text-align: center;
top: 0;
z-index: 100;
@media (max-width: 768px) {
align-items: stretch;
display: flex;
flex-direction: column;
padding: var(--space-md) 0;
}
}
.container,
.nav-menu {
display: grid;
grid-template-columns: 1fr auto 1fr;
justify-content: space-between;
align-items: center;
}
.mobile-nav-header {
anchor-name: --mobile-nav-header;
display: none;
justify-content: space-between;
align-items: center;
@media (max-width: 768px) {
display: flex;
}
}
.nav-header-logo {
color: var(--color-text);
display: inline-block;
max-width: 250px;
padding-left: var(--space-md);
&.mobile {
display: none;
}
@media (max-width: 768px) {
&.desktop {
display: none;
}
&.mobile {
display: inline-block;
}
}
}
/* Hamburger toggle: mobile only, animates to X when open */
.nav-toggle-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
&:checked {
& ~ .mobile-nav-header .nav-toggle .nav-toggle-line {
&:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
&:nth-child(2) {
opacity: 0;
}
&:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
}
& ~ .nav-menu {
max-height: 80vh;
opacity: 1;
padding-top: var(--space-md);
padding-bottom: var(--space-md);
border-top: 1px solid var(--color-border);
}
}
}
.btn-clear {
background: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}
.nav-toggle {
display: none;
align-items: center;
justify-content: flex-end;
flex: 0 0 auto;
padding: var(--space-sm) var(--space-md);
width: calc(24px + var(--space-md) + var(--space-md));
height: calc(31px + var(--space-sm) + var(--space-sm));
cursor: pointer;
color: var(--color-text);
background: transparent;
border: none;
border-radius: var(--space-xs);
transition:
color 0.2s ease,
background-color 0.2s ease;
@media (max-width: 768px) {
display: flex;
}
&:hover {
color: var(--color-primary);
}
&:focus-visible {
outline: 4px solid var(--color-focus);
outline-offset: 4px;
}
}
.nav-toggle-inner {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 24px;
height: 18px;
}
.nav-toggle-line {
display: block;
width: 100%;
height: 2px;
background-color: currentColor;
border-radius: 1px;
transform-origin: center;
transition:
transform 0.25s ease,
opacity 0.2s ease;
@media (prefers-reduced-motion: reduce) {
transition-duration: 0.01ms;
}
}
.nav-list {
display: flex;
justify-content: space-between;
align-items: center;
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
margin: 0 var(--space-md);
}
/* Back to top + mobile nav logo: hidden until page is scrolled (CSS scroll-driven animation) */
.nav-back-to-top,
.nav-header-logo.page-home {
/* Fallback when scroll-driven animations arent supported: always visible */
opacity: 1;
visibility: visible;
}
/* Mobile: show toggle, collapse menu until opened; menu overlays content via anchor */
@media (max-width: 768px) {
.nav-menu {
display: flex;
flex-direction: column;
align-items: stretch;
max-height: 0;
overflow: hidden;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
border-top: none;
transition:
max-height 0.3s ease,
opacity 0.25s ease,
padding 0.25s ease;
& .nav-list,
& .nav-item {
width: 100%;
}
& .nav-list {
flex-direction: column;
gap: 0;
}
& .nav-item {
margin: 0;
text-align: center;
& a:hover {
border-bottom-color: transparent;
}
}
& .nav-item a,
& .nav-back-to-top a {
display: block;
padding: var(--space-md);
}
@supports (top: anchor(bottom)) {
position: fixed;
position-anchor: --mobile-nav-header;
top: anchor(--mobile-nav-header bottom);
left: anchor(--mobile-nav-header left);
right: anchor(--mobile-nav-header right);
margin: 0;
overflow-y: auto;
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 99;
}
@media (prefers-reduced-motion: reduce) {
transition-duration: 0.01ms;
}
}
}
@supports (animation-timeline: scroll()) {
/* Shadow on pseudo-element; only opacity is animated (composited) */
.nav::after {
content: '';
position: absolute;
inset: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
pointer-events: none;
opacity: 0;
animation: nav-shadow-on-scroll linear;
animation-timeline: scroll(root block);
animation-range: 0 100px;
animation-fill-mode: both;
will-change: opacity;
}
@keyframes nav-shadow-on-scroll {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.nav::after {
animation: none;
opacity: 1;
}
}
/* Composited-only animation: opacity only (visibility/pointer-events not animated) */
.nav-back-to-top,
.nav-header-logo.page-home {
opacity: 0;
animation: nav-reveal-on-scroll linear;
animation-timeline: scroll(root block);
animation-range: 300px 400px;
animation-fill-mode: both;
will-change: opacity;
}
@keyframes nav-reveal-on-scroll {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.nav-back-to-top,
.nav-header-logo {
animation: none;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}
}
</style>