Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/components/ConversionTracking.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
// Google Ads conversion wiring for any `a[data-conversion]` on the page.
// Include once per component that renders conversion links (Astro dedupes the
// bundled script); the per-element guard keeps a link from ever being bound
// twice even if two components include it on the same page.
---

<script>
// Google Ads conversion labels — the value after the slash from
// Google Ads → Tools → Conversions → "<name>" → Tag setup.
const CONVERSIONS: Record<string, string> = {
mailto_click: 'AW-18128547216/wIcbCOfc66QcEJDbrsRD',
};

function fireConversion(name: string | undefined) {
const sendTo = name && CONVERSIONS[name];
// @ts-expect-error gtag is injected globally by Base.astro
if (!sendTo || typeof gtag !== 'function') return;
// @ts-expect-error gtag is injected globally by Base.astro
gtag('event', 'conversion', { send_to: sendTo });
}

document.querySelectorAll<HTMLAnchorElement>('a[data-conversion]').forEach((el) => {
if (el.dataset.convBound) return;
el.dataset.convBound = '1';
el.addEventListener('click', () => fireConversion(el.dataset.conversion));
});
</script>
111 changes: 73 additions & 38 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@ import { resolveLocale, localesForPath, localizedPath, t } from '../lib/i18n';

const { code: lang } = resolveLocale(Astro.url.pathname);
const ui = t(lang);
const aboutHref = localesForPath('/about/').includes(lang) ? localizedPath('/about/', lang) : '/about/';
const localeHref = (path: string) =>
localesForPath(path).includes(lang) ? localizedPath(path, lang) : path;

// Sitemap links reuse the localized chrome strings — no new translations needed.
const links = [
{ href: localeHref('/voice/'), label: ui.navVoice },
{ href: localeHref('/voice/download/'), label: ui.subDownload },
{ href: localeHref('/voice/changelog/'), label: ui.subChangelog },
{ href: '/docs', label: ui.navDocs },
{ href: localeHref('/blog/'), label: ui.navBlog },
{ href: localeHref('/about/'), label: ui.footerAbout },
];

let siteVersion: string;
try {
Expand All @@ -16,48 +27,72 @@ try {
}
---

<footer class="border-t border-gray-100 dark:border-white/10 pt-6 flex items-center justify-between text-xs text-gray-400 dark:text-gray-600">
<div class="flex items-center gap-3">
<a
href="https://github.com/wavekat"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
<span class="sr-only">GitHub</span>
</a>
<footer class="mt-20 border-t border-gray-100 bg-gray-50/60 dark:border-white/10 dark:bg-wk-surface/40">
<div class="mx-auto max-w-6xl px-6 pt-12">
<div class="flex flex-col gap-8 sm:flex-row sm:items-start sm:justify-between">
<div class="flex flex-col gap-4">
<a href={localizedPath('/', lang)} class="block w-fit">
<img src="/logos/wavekat-tight-light.svg" alt="wavekat" width="97" height="28" class="h-7 dark:hidden" />
<img src="/logos/wavekat-tight-dark.svg" alt="wavekat" width="97" height="28" class="h-7 hidden dark:block" />
</a>
<div class="flex items-center gap-1 text-gray-400 dark:text-gray-500">
<a
href="https://github.com/wavekat"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
class="rounded-full p-2 transition-colors hover:bg-gray-900/5 hover:text-gray-700 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path></svg>
<span class="sr-only">GitHub</span>
</a>
<a
href="https://www.linkedin.com/company/wavekat/"
target="_blank"
rel="noopener noreferrer"
title="LinkedIn"
class="rounded-full p-2 transition-colors hover:bg-gray-900/5 hover:text-gray-700 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg>
<span class="sr-only">LinkedIn</span>
</a>
<a
href="mailto:eason@wavekat.com"
title={ui.footerEmail}
class="rounded-full p-2 transition-colors hover:bg-gray-900/5 hover:text-gray-700 dark:hover:bg-white/5 dark:hover:text-gray-300"
>
<Mail class="w-5 h-5" />
<span class="sr-only">{ui.footerEmail}</span>
</a>
</div>
</div>
<nav class="grid grid-cols-2 gap-x-10 gap-y-2.5 sm:text-right">
{links.map((link) => (
<a
href={link.href}
class="text-sm text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
{link.label}
</a>
))}
</nav>
</div>
<div class="mt-10 flex items-center justify-between border-t border-gray-100 py-5 text-xs text-gray-400 dark:border-white/10 dark:text-gray-600">
<span>© {new Date().getFullYear()} WaveKat</span>
<a
href="https://www.linkedin.com/company/wavekat/"
href="https://github.com/wavekat/wavekat.com"
target="_blank"
rel="noopener noreferrer"
title="LinkedIn"
class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg>
<span class="sr-only">LinkedIn</span>
</a>
<a
href="mailto:eason@wavekat.com"
title={ui.footerEmail}
class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors"
title={ui.footerSource}
class="font-mono transition-colors hover:text-gray-600 dark:hover:text-gray-400"
>
<Mail class="w-5 h-5" />
<span class="sr-only">{ui.footerEmail}</span>
{siteVersion}
</a>
</div>
<a
href="https://github.com/wavekat/wavekat.com"
target="_blank"
rel="noopener noreferrer"
title={ui.footerSource}
class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors"
>
{siteVersion}
</a>
<div class="flex items-center gap-3">
<a href={aboutHref} class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors">{ui.footerAbout}</a>
<span>© {new Date().getFullYear()} WaveKat</span>
</div>
<!-- Giant watermark wordmark peeking from the bottom edge — pure brand, no copy. -->
<div class="relative mx-auto h-16 max-w-6xl overflow-hidden px-6 sm:h-24 md:h-32" aria-hidden="true">
<img src="/logos/wavekat-tight-light.svg" alt="" class="absolute top-2 left-6 right-6 w-[calc(100%-3rem)] opacity-[0.06] dark:hidden" loading="lazy" />
<img src="/logos/wavekat-tight-dark.svg" alt="" class="absolute top-2 left-6 right-6 hidden w-[calc(100%-3rem)] opacity-[0.05] dark:block" loading="lazy" />
</div>
</footer>
55 changes: 33 additions & 22 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,44 @@ const localeHref = (path: string) =>
localesForPath(path).includes(lang) ? localizedPath(path, lang) : path;

// `whitespace-nowrap` keeps each label on one line — without it the flex row
// squeezes the pills on a narrow (mobile) viewport and CJK labels (语音, 简体中文)
// squeezes the links on a narrow (mobile) viewport and CJK labels (语音, 简体中文)
// break between characters, since CJK text has no word boundaries. The header
// wraps the nav group to its own line instead (flex-wrap below).
const pillClass = (isActive: boolean) =>
const linkClass = (isActive: boolean) =>
isActive
? 'text-xs font-bold rounded px-3 py-1 whitespace-nowrap text-gray-700 dark:text-gray-200 border border-gray-400 dark:border-white/30 transition-colors'
: 'text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors border border-gray-200 dark:border-white/10 rounded px-3 py-1 whitespace-nowrap';
? 'text-sm font-semibold whitespace-nowrap px-2.5 py-1 rounded-full text-wk-orange bg-wk-orange/10 transition-colors'
: 'text-sm font-medium whitespace-nowrap px-2.5 py-1 rounded-full text-gray-500 hover:text-gray-900 hover:bg-gray-900/5 dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/5 transition-colors';
---

<header class="flex flex-wrap items-center justify-between gap-y-3 mb-12">
<a href={homeHref} class="block">
<img src="/logos/wavekat-tight-light.svg" alt="wavekat" width="125" height="36" class="h-9 dark:hidden" />
<img src="/logos/wavekat-tight-dark.svg" alt="wavekat" width="125" height="36" class="h-9 hidden dark:block" />
</a>
<div class="flex items-center gap-3">
<a href={localeHref('/voice/')} class={pillClass(active === 'voice')}>{ui.navVoice}</a>
<a href="/docs" class={pillClass(active === 'docs')}>{ui.navDocs}</a>
<a href={localeHref('/blog/')} class={pillClass(active === 'blog')}>{ui.navBlog}</a>
<LanguageSwitcher />
<button
id="theme-toggle"
aria-label="Toggle theme"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
>
<Sun class="w-4 h-4 dark:hidden" />
<Moon class="w-4 h-4 hidden dark:block" />
</button>
<header class="sticky top-0 z-40 border-b border-gray-200/60 bg-white/80 backdrop-blur-xl dark:border-white/5 dark:bg-wk-bg/80">
<div
class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-4 gap-y-2 px-6 py-3"
>
<a href={homeHref} class="block shrink-0">
<img src="/logos/wavekat-tight-light.svg" alt="wavekat" width="111" height="32" class="h-8 dark:hidden" />
<img src="/logos/wavekat-tight-dark.svg" alt="wavekat" width="111" height="32" class="h-8 hidden dark:block" />
</a>
<nav class="flex flex-wrap items-center gap-1.5">
<a href={localeHref('/voice/')} class={linkClass(active === 'voice')}>{ui.navVoice}</a>
<a href="/docs" class={linkClass(active === 'docs')}>{ui.navDocs}</a>
<a href={localeHref('/blog/')} class={linkClass(active === 'blog')}>{ui.navBlog}</a>
<span class="mx-1 hidden h-4 w-px bg-gray-200 dark:bg-white/10 sm:block" aria-hidden="true"></span>
<LanguageSwitcher />
<button
id="theme-toggle"
aria-label="Toggle theme"
class="rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-900/5 hover:text-gray-700 dark:hover:bg-white/5 dark:hover:text-gray-200"
>
<Sun class="w-4 h-4 dark:hidden" />
<Moon class="w-4 h-4 hidden dark:block" />
</button>
<a
href={localeHref('/voice/download/')}
class="hidden whitespace-nowrap rounded-full bg-wk-orange px-4 py-1.5 text-xs font-semibold text-white shadow-md shadow-wk-orange/25 transition-all hover:-translate-y-px hover:shadow-lg hover:shadow-wk-orange/35 sm:inline-flex"
>
{ui.subDownload}
</a>
</nav>
</div>
</header>

Expand Down
4 changes: 2 additions & 2 deletions src/components/LanguageSwitcher.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const label = t(lang).switcherLabel;
aria-expanded="false"
aria-label={label}
data-lang-trigger
class="flex items-center gap-1.5 whitespace-nowrap rounded border border-gray-200 px-2.5 py-1 text-xs text-gray-500 transition-colors hover:text-gray-700 dark:border-white/10 dark:text-gray-400 dark:hover:text-gray-200"
class="flex items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-1.5 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-900/5 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200"
>
<Globe class="h-3.5 w-3.5" />
<span>{current}</span>
Expand All @@ -34,7 +34,7 @@ const label = t(lang).switcherLabel;
data-lang-menu
hidden
role="menu"
class="absolute right-0 top-full z-40 mt-2 hidden min-w-[10rem] flex-col gap-0.5 rounded-lg border border-gray-200 bg-white p-1.5 shadow-xl dark:border-white/10 dark:bg-wk-surface"
class="absolute right-0 top-full z-40 mt-2 hidden min-w-[10rem] flex-col gap-0.5 rounded-xl border border-gray-200 bg-white/95 p-1.5 shadow-xl backdrop-blur-xl dark:border-white/10 dark:bg-wk-surface/95"
>
{items.map((item) => (
<a
Expand Down
7 changes: 4 additions & 3 deletions src/components/NotFound.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const ui = t(lang);
---

<Base title={`${ui.notFoundTitle} — WaveKat`} description={ui.notFoundBody} noindex>
<div class="max-w-3xl mx-auto px-6 py-12">
<Header />

<Header />
<div class="max-w-6xl mx-auto px-6 py-12">

<section class="mb-20 py-16 text-center">
<p class="text-6xl font-bold mb-6" style="color: #ff6d00;">404</p>
Expand All @@ -30,6 +30,7 @@ const ui = t(lang);
</div>
</section>

<Footer />
</div>

<Footer />
</Base>
14 changes: 7 additions & 7 deletions src/components/PostCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ const formatDate = (d: Date) =>

<a
href={`${hrefBase}${slug}`}
class="group block rounded border border-gray-200 dark:border-white/10 p-5 dark:bg-wk-surface bg-gray-50 hover:bg-gray-100 dark:hover:bg-[#0d1520] transition-colors"
class="group block rounded-2xl border border-gray-200/80 bg-gray-50/50 p-6 transition-all hover:-translate-y-0.5 hover:border-wk-orange/40 hover:shadow-lg hover:shadow-wk-orange/5 dark:border-white/10 dark:bg-wk-surface dark:hover:border-wk-orange/40"
>
<time datetime={date.toISOString()} class="text-[10px] text-gray-400 dark:text-gray-600 tracking-wide uppercase">
<time datetime={date.toISOString()} class="font-mono text-[11px] uppercase tracking-wider text-gray-400 dark:text-gray-500">
{formatDate(date)}
</time>
<h3 class="text-sm font-bold text-gray-900 dark:text-white mt-1.5 mb-1.5 group-hover:underline underline-offset-2">
<h3 class="mt-2 mb-2 text-base font-semibold leading-snug text-gray-900 transition-colors group-hover:text-wk-orange dark:text-white">
{title}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 leading-relaxed mb-3">
<p class="mb-4 text-sm leading-relaxed text-gray-500 dark:text-gray-400">
{description}
</p>
{tags.length > 0 && (
<div class="flex flex-wrap gap-3">
<div class="flex flex-wrap gap-2">
{tags.map((tag) => (
<span class="text-[10px] tracking-wider uppercase text-gray-400 dark:text-gray-600">
#{tag}
<span class="rounded-full bg-gray-900/5 px-2.5 py-0.5 font-mono text-[10px] tracking-wider text-gray-500 dark:bg-white/5 dark:text-gray-400">
{tag}
</span>
))}
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/components/SiteBackground.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
// wavelengths/speeds keep the field from ever looking like it repeats.
// baseAmp/baseAlpha are the calm resting state — deliberately low.
const layers = [
{ y: 0.16, amp: 14, len: 0.0065, speed: 0.55, phase: 0.0, alpha: 0.05, width: 2 },
{ y: 0.34, amp: 18, len: 0.0042, speed: -0.4, phase: 1.7, alpha: 0.038, width: 2.5 },
{ y: 0.52, amp: 16, len: 0.0055, speed: 0.7, phase: 3.1, alpha: 0.045, width: 2 },
{ y: 0.7, amp: 20, len: 0.0035, speed: -0.5, phase: 4.6, alpha: 0.035, width: 3 },
{ y: 0.86, amp: 15, len: 0.006, speed: 0.45, phase: 5.9, alpha: 0.042, width: 2 },
{ y: 0.16, amp: 14, len: 0.0065, speed: 0.55, phase: 0.0, alpha: 0.09, width: 2 },
{ y: 0.34, amp: 18, len: 0.0042, speed: -0.4, phase: 1.7, alpha: 0.07, width: 2.5 },
{ y: 0.52, amp: 16, len: 0.0055, speed: 0.7, phase: 3.1, alpha: 0.08, width: 2 },
{ y: 0.7, amp: 20, len: 0.0035, speed: -0.5, phase: 4.6, alpha: 0.065, width: 3 },
{ y: 0.86, amp: 15, len: 0.006, speed: 0.45, phase: 5.9, alpha: 0.075, width: 2 },
];

let w = 0;
Expand Down
Loading
Loading