feat: enhance header and mobile navigation with smooth scrolling and active link highlighting

This commit is contained in:
TerryM
2026-05-12 22:39:36 +08:00
parent dbaad19d0b
commit a6bd0ca864
4 changed files with 134 additions and 19 deletions

View File

@@ -9,11 +9,11 @@
<div class="flex flex-col items-start w-full"> <div class="flex flex-col items-start w-full">
<!-- 3 cards row --> <!-- 3 cards row -->
<div class="flex flex-col lg:flex-row lg:gap-[24px] lg:h-[477px] lg:items-center gap-6 w-full shrink-0"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 w-full">
<!-- Card 1: Clear Interface --> <!-- Card 1: Clear Interface -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] lg:h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-full lg:w-[320px]"> <div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] w-full">
<div class="relative h-[232px] shrink-0 w-full lg:w-[320px]"> <div class="relative h-[232px] shrink-0 w-full">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Clear interface screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-58.58%] w-full" src="/assets/exp-card-1.png" /> <img alt="Clear interface screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-58.58%] w-full" src="/assets/exp-card-1.png" />
</div> </div>
@@ -25,8 +25,8 @@
</div> </div>
<!-- Card 2: Smooth Navigation --> <!-- Card 2: Smooth Navigation -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] lg:h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-full lg:w-[320px]"> <div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] w-full">
<div class="bg-[#ffeddf] flex flex-col items-start shrink-0 w-full lg:w-[320px]"> <div class="bg-[#ffeddf] flex flex-col items-start w-full">
<div class="relative h-[232px] shrink-0 w-full"> <div class="relative h-[232px] shrink-0 w-full">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Smooth navigation screenshot" class="absolute h-[411.28%] left-[-5.95%] max-w-none top-[-335.6%] w-[137.79%]" src="/assets/exp-card-2.png" /> <img alt="Smooth navigation screenshot" class="absolute h-[411.28%] left-[-5.95%] max-w-none top-[-335.6%] w-[137.79%]" src="/assets/exp-card-2.png" />
@@ -40,8 +40,8 @@
</div> </div>
<!-- Card 3: Refined Visual Design --> <!-- Card 3: Refined Visual Design -->
<div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] lg:h-full items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] shrink-0 w-full lg:w-[320px]"> <div class="bg-gradient-to-b from-[#fef0eb] to-white flex flex-col gap-[36px] items-center overflow-clip pb-[36px] px-[36px] rounded-tl-[30px] rounded-tr-[30px] w-full">
<div class="bg-[#ffeddf] flex flex-col h-[232px] items-start overflow-clip shrink-0 w-full lg:w-[320px]"> <div class="bg-[#ffeddf] flex flex-col h-[232px] items-start overflow-clip w-full">
<div class="flex-1 min-h-0 relative w-full"> <div class="flex-1 min-h-0 relative w-full">
<div class="absolute inset-0 overflow-hidden pointer-events-none"> <div class="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="Refined visual design screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-99.23%] w-full" src="/assets/exp-card-3.png" /> <img alt="Refined visual design screenshot" class="absolute h-[298.5%] left-0 max-w-none top-[-99.23%] w-full" src="/assets/exp-card-3.png" />

View File

@@ -4,8 +4,9 @@ const logoWordmark = "/assets/header-logo-wordmark.svg";
const globeIcon = "/assets/header-globe.svg"; const globeIcon = "/assets/header-globe.svg";
--- ---
<header class="bg-white border-b border-[#e3d9d1] w-full flex items-center justify-center h-[72px] sticky top-0 z-50"> <header id="site-header" class="bg-white border-b border-[#e3d9d1] w-full sticky top-0 z-50">
<div class="flex items-center w-full max-w-[1280px] mx-auto px-4 lg:px-6"> <!-- Main bar -->
<div class="flex items-center w-full max-w-[1280px] mx-auto px-4 lg:px-6 h-[72px] gap-6">
<!-- Logo --> <!-- Logo -->
<div class="flex flex-1 items-center min-w-0"> <div class="flex flex-1 items-center min-w-0">
<a href="/" class="relative h-[42px] w-[143px] shrink-0 block"> <a href="/" class="relative h-[42px] w-[143px] shrink-0 block">
@@ -18,24 +19,98 @@ const globeIcon = "/assets/header-globe.svg";
</a> </a>
</div> </div>
<!-- Nav --> <!-- Desktop nav -->
<nav class="hidden lg:flex items-center gap-[32px] shrink-0"> <nav class="hidden lg:flex items-center gap-[32px] shrink-0" aria-label="Main navigation">
<a href="#" class="font-semibold text-[14px] text-[#f28a4b] tracking-[-0.09px] whitespace-nowrap leading-normal">Home</a> <a href="#hero" class="font-semibold text-[14px] text-[#f28a4b] tracking-[-0.09px] whitespace-nowrap leading-normal" data-nav-link>Home</a>
<a href="#features" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Features</a> <a href="#features" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors" data-nav-link>Features</a>
<a href="#experience" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Experience</a> <a href="#experience" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors" data-nav-link>Experience</a>
<a href="#use-cases" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Use Cases</a> <a href="#use-cases" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors" data-nav-link>Use Cases</a>
<a href="#reliability" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors">Reliability</a> <a href="#reliability" class="font-semibold text-[14px] text-[#7a726d] tracking-[-0.09px] whitespace-nowrap leading-normal hover:text-[#f28a4b] transition-colors" data-nav-link>Reliability</a>
</nav> </nav>
<!-- Actions --> <!-- Actions -->
<div class="flex flex-1 gap-[12px] items-center justify-end min-w-0"> <div class="flex flex-1 gap-[12px] items-center justify-end min-w-0">
<!-- Language button (desktop only) -->
<button class="hidden lg:flex border border-[rgba(46,42,40,0.3)] gap-[8px] h-[43px] items-center justify-center pl-[12px] pr-[16px] rounded-[17px] shrink-0 hover:border-[#f28a4b] transition-colors"> <button class="hidden lg:flex border border-[rgba(46,42,40,0.3)] gap-[8px] h-[43px] items-center justify-center pl-[12px] pr-[16px] rounded-[17px] shrink-0 hover:border-[#f28a4b] transition-colors">
<img alt="Language" class="block size-[26px] object-contain" src={globeIcon} /> <img alt="Language" class="block size-[26px] object-contain" src={globeIcon} />
<span class="font-semibold text-[14px] text-[#2e2a28] tracking-[-0.09px] whitespace-nowrap leading-[14px]">EN</span> <span class="font-semibold text-[14px] text-[#2e2a28] tracking-[-0.09px] whitespace-nowrap leading-[14px]">EN</span>
</button> </button>
<a href="#download" class="bg-[#f28a4b] flex items-center justify-center px-[22px] py-[13px] rounded-[17px] shrink-0 hover:bg-[#e07a3b] transition-colors"> <!-- Download button (desktop only) -->
<a href="#download" class="hidden lg:flex bg-[#f28a4b] items-center justify-center px-[22px] py-[13px] rounded-[17px] shrink-0 hover:bg-[#e07a3b] transition-colors">
<span class="font-bold text-[14px] text-white leading-normal whitespace-nowrap">Download</span> <span class="font-bold text-[14px] text-white leading-normal whitespace-nowrap">Download</span>
</a> </a>
<!-- Hamburger toggle (mobile only) -->
<button
id="menu-toggle"
class="lg:hidden flex flex-col justify-center gap-[5px] w-[40px] h-[40px] p-[8px] shrink-0"
type="button"
aria-label="Open menu"
aria-expanded="false"
aria-controls="mobile-nav"
>
<span id="bar-1" class="block w-full h-[2px] bg-[#2e2a28] rounded-sm transition-all duration-[240ms] origin-center"></span>
<span id="bar-2" class="block w-full h-[2px] bg-[#2e2a28] rounded-sm transition-all duration-[240ms]"></span>
<span id="bar-3" class="block w-full h-[2px] bg-[#2e2a28] rounded-sm transition-all duration-[240ms] origin-center"></span>
</button>
</div> </div>
</div> </div>
<!-- Mobile nav drawer -->
<div id="mobile-nav" class="hidden lg:!hidden border-t border-[#e3d9d1] bg-white" aria-hidden="true">
<ul class="flex flex-col px-6 py-4 list-none m-0 p-0 px-6 py-4">
<li><a href="#hero" class="block py-4 font-semibold text-[16px] text-[#2e2a28] border-b border-[#e3d9d1]" data-nav-link>Home</a></li>
<li><a href="#features" class="block py-4 font-semibold text-[16px] text-[#2e2a28] border-b border-[#e3d9d1]" data-nav-link>Features</a></li>
<li><a href="#experience" class="block py-4 font-semibold text-[16px] text-[#2e2a28] border-b border-[#e3d9d1]" data-nav-link>Experience</a></li>
<li><a href="#use-cases" class="block py-4 font-semibold text-[16px] text-[#2e2a28] border-b border-[#e3d9d1]" data-nav-link>Use Cases</a></li>
<li><a href="#reliability" class="block py-4 font-semibold text-[16px] text-[#2e2a28]" data-nav-link>Reliability</a></li>
<li class="mt-4 mb-2">
<a href="#download" class="block text-center bg-[#f28a4b] text-white font-bold text-[15px] py-[14px] px-6 rounded-[17px] hover:bg-[#e07a3b] transition-colors">
Download
</a>
</li>
</ul>
</div>
</header> </header>
<script>
const toggle = document.getElementById('menu-toggle') as HTMLButtonElement;
const nav = document.getElementById('mobile-nav') as HTMLDivElement;
const bar1 = document.getElementById('bar-1') as HTMLSpanElement;
const bar2 = document.getElementById('bar-2') as HTMLSpanElement;
const bar3 = document.getElementById('bar-3') as HTMLSpanElement;
function openMenu() {
toggle.setAttribute('aria-expanded', 'true');
toggle.setAttribute('aria-label', 'Close menu');
nav.classList.remove('hidden');
nav.removeAttribute('aria-hidden');
// Animate to X
bar1.style.transform = 'translateY(7px) rotate(45deg)';
bar2.style.opacity = '0';
bar3.style.transform = 'translateY(-7px) rotate(-45deg)';
}
function closeMenu() {
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-label', 'Open menu');
nav.classList.add('hidden');
nav.setAttribute('aria-hidden', 'true');
// Reset bars
bar1.style.transform = '';
bar2.style.opacity = '';
bar3.style.transform = '';
}
toggle.addEventListener('click', () => {
toggle.getAttribute('aria-expanded') === 'true' ? closeMenu() : openMenu();
});
// Close when any nav link is clicked
nav.querySelectorAll('a').forEach(a => a.addEventListener('click', closeMenu));
// Close when resized past mobile breakpoint
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) closeMenu();
});
</script>

View File

@@ -3,7 +3,7 @@ const heroBg = "/assets/hero-bg.png";
const phoneMockup = "/assets/hero-phone.png"; const phoneMockup = "/assets/hero-phone.png";
--- ---
<section class="relative w-full flex items-start justify-center min-h-[600px] lg:h-[891px] overflow-hidden"> <section id="hero" class="relative w-full flex items-start justify-center min-h-[600px] lg:h-[891px] overflow-hidden">
<img alt="" class="absolute inset-0 max-w-none object-cover size-full pointer-events-none" src={heroBg} /> <img alt="" class="absolute inset-0 max-w-none object-cover size-full pointer-events-none" src={heroBg} />
<div class="relative flex flex-col-reverse lg:flex-row gap-[40px] items-start w-full max-w-[1280px] mx-auto px-4 lg:px-0 h-full"> <div class="relative flex flex-col-reverse lg:flex-row gap-[40px] items-start w-full max-w-[1280px] mx-auto px-4 lg:px-0 h-full">

View File

@@ -16,9 +16,49 @@ const { title = 'Talk Pro — One User. Multiple Worlds.' } = Astro.props
<title>{title}</title> <title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head> </head>
<body class="bg-surface font-sans overflow-x-hidden"> <body class="bg-surface font-sans overflow-x-hidden">
<slot /> <slot />
<script>
(() => {
const header = document.getElementById('site-header');
const getOffset = () => header ? header.offsetHeight : 0;
// Smooth scroll with header offset
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', e => {
const href = link.getAttribute('href');
if (!href || href === '#') return;
const target = document.querySelector(href);
if (!target) return;
e.preventDefault();
const top = target.getBoundingClientRect().top + window.scrollY - getOffset();
window.scrollTo({ top, behavior: 'smooth' });
});
});
// Active nav highlighting via IntersectionObserver
const navLinks = document.querySelectorAll('[data-nav-link]');
const sections = Array.from(navLinks)
.map(l => document.querySelector(l.getAttribute('href') ?? ''))
.filter(Boolean) as Element[];
if ('IntersectionObserver' in window && sections.length) {
const observer = new IntersectionObserver(entries => {
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (!visible) return;
navLinks.forEach(link => {
const active = link.getAttribute('href') === `#${visible.target.id}`;
link.classList.toggle('!text-[#f28a4b]', active);
link.classList.toggle('text-[#7a726d]', !active);
});
}, { rootMargin: '-30% 0px -60% 0px', threshold: 0 });
sections.forEach(s => observer.observe(s));
}
})();
</script>
</body> </body>
</html> </html>