Files
website/apps/website/components/AppNavbar.vue
matt 5055612614
All checks were successful
build-website / build (push) Successful in 1m36s
Clamp nav safe area and fix hero video offsets
2025-09-21 15:04:42 +02:00

176 lines
4.2 KiB
Vue

<template>
<nav
id="voyageNav"
:class="['voyage-nav', { scrolled: isScrolled }]"
>
<div class="nav-container">
<div class="nav-brand" @click="scrollToTop" style="cursor: pointer;">
<img
:src="navLogo"
:alt="logoAlt"
class="nav-logo"
id="navLogo"
width="150"
height="50"
>
<span>HARBOR SMITH</span>
</div>
<div class="nav-links">
<a
v-for="link in navLinks"
:key="link.href"
:href="link.href"
class="nav-link"
@click="handleSmoothScroll($event, link.href)"
>
{{ link.label }}
</a>
<a
href="tel:510-701-2535"
class="nav-link nav-cta"
>
Call Now
</a>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useSmoothScroll } from '~/composables/useSmoothScroll'
const { scrollToElement, scrollToTop: smoothScrollToTop } = useSmoothScroll()
const isScrolled = ref(false)
const navLinks = [
{ href: '#services', label: 'Services' },
{ href: '#testimonials', label: 'Testimonials' },
{ href: '#contact', label: 'Contact' }
]
const navLogo = computed(() =>
isScrolled.value ? '/HARBOR-SMITH_navy.png' : '/HARBOR-SMITH-white.png'
)
const logoAlt = computed(() =>
isScrolled.value ? 'Harbor Smith Navy Logo' : 'Harbor Smith White Logo'
)
let maxSafeAreaTop = 0
const measureSafeAreaTop = () => {
if (typeof window === 'undefined') {
return 0
}
const viewport = window.visualViewport
const viewportInset = viewport?.offsetTop ?? 0
let envInset = 0
let legacyInset = 0
if (document.body) {
const probe = document.createElement('div')
probe.style.cssText = [
'position:absolute',
'top:0',
'left:0',
'width:0',
'height:env(safe-area-inset-top, 0px)',
'pointer-events:none',
'visibility:hidden'
].join(';')
document.body.appendChild(probe)
envInset = probe.getBoundingClientRect().height || 0
probe.style.height = 'constant(safe-area-inset-top)'
legacyInset = probe.getBoundingClientRect().height || 0
document.body.removeChild(probe)
}
return Math.max(0, viewportInset, envInset, legacyInset)
}
const setInitialSafeAreaTop = () => {
if (typeof window === 'undefined') {
return
}
const root = document.documentElement
const measured = measureSafeAreaTop()
if (measured > maxSafeAreaTop) {
maxSafeAreaTop = measured
root.style.setProperty('--initial-safe-area-top', `${measured}px`)
}
}
const resetSafeAreaTop = () => {
if (typeof window === 'undefined') {
return
}
maxSafeAreaTop = 0
document.documentElement.style.setProperty('--initial-safe-area-top', '0px')
const remeasure = () => {
setInitialSafeAreaTop()
}
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => {
requestAnimationFrame(remeasure)
})
} else {
setTimeout(remeasure, 50)
}
}
const handleScroll = () => {
isScrolled.value = window.pageYOffset > 50
}
const handleSmoothScroll = (event: Event, href: string) => {
if (!href.startsWith('#')) {
return
}
event.preventDefault()
scrollToElement(href, 800) // Constant-speed smooth scrolling
}
const scrollToTop = () => {
smoothScrollToTop(800) // Constant-speed smooth scrolling
}
onMounted(() => {
setInitialSafeAreaTop()
setTimeout(() => {
setInitialSafeAreaTop()
}, 100)
handleScroll()
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('orientationchange', resetSafeAreaTop)
window.addEventListener('resize', setInitialSafeAreaTop)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setInitialSafeAreaTop)
}
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('orientationchange', resetSafeAreaTop)
window.removeEventListener('resize', setInitialSafeAreaTop)
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', setInitialSafeAreaTop)
}
maxSafeAreaTop = 0
})
</script>
<style scoped>
/* Navigation styling handled in voyage-layout.css */
</style>