Three taglines rotate every 3.5s with a subtle 700ms opacity crossfade: 1. Inspect any request. Mock any response. (default) 2. Your HTTP bin, on your machine. 3. Send. Capture. Mock. No signup. Implementation notes: - Client component, default tagline in initial SSR HTML for SEO/no-JS - ARIA: aria-live=polite on container, aria-hidden on inactive items, aria-atomic=true; prefers-reduced-motion suppresses both the timer and the fade (via motion-reduce:transition-none + early-return guard) - No CLS: container has fixed h-7 md:h-8 height; absolute children crossfade in place - Hierarchy: tagline below H1 (mb-6), above hero sub (mb-6 from tagline container, mb-10 from sub to CTAs) Tailwind v4 fixes that were needed to make this visible: - app/globals.css: switch to @import 'tailwindcss' with explicit @source globs (auto-detection misses components when Turbopack workspace root is ambiguous) - app/globals.css: drop redundant reset; Tailwind v4 Preflight handles it, and an unlayered selector beats @layer utilities so it was silently killing mb-*, mx-*, etc. - app/globals.css: wrap in @layer base so text-white / text-zinc-* utilities can override it - next.config.ts: pin turbopack.root to project dir so the build works regardless of shell cwd (was running into the paperclip workspace root during agent sessions)
112 lines
5.4 KiB
TypeScript
112 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
// Rotating taglines — Slot 3 above the hero sub (ELF-457, brief ELF-456).
|
|
// Order matters: index 0 is the default and the initial server-rendered string.
|
|
const TAGLINES = [
|
|
"Inspect any request. Mock any response.",
|
|
"Your HTTP bin, on your machine.",
|
|
"Send. Capture. Mock. No signup.",
|
|
] as const;
|
|
|
|
// Per-tagline dwell. 3.5s gives ~2.8s solid visibility after the 700ms crossfade.
|
|
const ROTATE_MS = 3500;
|
|
|
|
export default function Hero() {
|
|
const [index, setIndex] = useState(0);
|
|
|
|
useEffect(() => {
|
|
// Respect prefers-reduced-motion at the JS layer too: don't start a timer
|
|
// for users who opt out. The fade is independently suppressed via the
|
|
// motion-reduce: variant on the <p> class below.
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
const id = window.setInterval(() => {
|
|
setIndex((i) => (i + 1) % TAGLINES.length);
|
|
}, ROTATE_MS);
|
|
return () => window.clearInterval(id);
|
|
}, []);
|
|
|
|
return (
|
|
<section className="pt-32 pb-20 px-6">
|
|
<div className="max-w-4xl mx-auto text-center">
|
|
<div className="inline-block mb-6 px-5 py-2 rounded-full bg-blue-500/10 border border-blue-500/30 text-blue-400 text-sm font-medium">
|
|
Open source · MIT licensed
|
|
</div>
|
|
|
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight mb-6 leading-tight bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">
|
|
Self-hostable HTTP request<br />
|
|
inspector + mocker
|
|
</h1>
|
|
|
|
{/* Slot 3: rotating tagline. Fixed-height container prevents CLS; subtle
|
|
opacity crossfade so it doesn't compete with the terminal CTA below.
|
|
a11y: aria-live polite + aria-hidden on inactive items. */}
|
|
<div
|
|
className="relative h-7 md:h-8 mb-6 mx-auto max-w-2xl"
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
>
|
|
{TAGLINES.map((t, i) => (
|
|
<p
|
|
key={t}
|
|
aria-hidden={i !== index}
|
|
className={`absolute inset-0 font-mono text-sm md:text-lg font-semibold text-zinc-300 leading-7 md:leading-8 transition-opacity duration-700 ease-out motion-reduce:transition-none ${
|
|
i === index ? "opacity-100" : "opacity-0"
|
|
}`}
|
|
>
|
|
{t}
|
|
</p>
|
|
))}
|
|
</div>
|
|
|
|
<p className="text-lg md:text-xl text-zinc-400 mb-10 max-w-2xl mx-auto leading-relaxed">
|
|
One Go binary. SQLite under the hood.
|
|
<code className="mx-1 px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-base">
|
|
docker compose up
|
|
</code>
|
|
, and you have an inspection endpoint and a live feed in under 30 seconds.
|
|
</p>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-16">
|
|
<a
|
|
href="https://github.com/elfoundation/hatch"
|
|
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-all shadow-lg shadow-blue-500/30 hover:shadow-xl hover:shadow-blue-500/40 hover:-translate-y-0.5"
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
|
</svg>
|
|
View on GitHub
|
|
</a>
|
|
<a
|
|
href="https://github.com/elfoundation/hatch#quick-start"
|
|
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-zinc-800 hover:bg-zinc-700 text-white font-semibold rounded-lg border border-zinc-700 transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5"
|
|
>
|
|
Quick Start →
|
|
</a>
|
|
</div>
|
|
|
|
{/* Terminal */}
|
|
<div className="max-w-2xl mx-auto rounded-xl overflow-hidden border border-zinc-800 bg-zinc-900/50 backdrop-blur-sm shadow-2xl">
|
|
<div className="flex items-center gap-2 px-4 py-3 bg-gradient-to-r from-zinc-800/50 to-zinc-800/30 border-b border-zinc-800">
|
|
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
|
|
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
|
|
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
|
|
<span className="ml-2 text-sm text-zinc-500">terminal</span>
|
|
</div>
|
|
<pre className="p-5 text-left text-sm font-mono overflow-x-auto">
|
|
<code>
|
|
<span className="text-blue-400 font-semibold">$</span>{' '}
|
|
<span className="text-zinc-300">docker compose up -d</span>{'\n'}
|
|
<span className="text-emerald-400">✓ hatch started on :8080</span>{'\n'}
|
|
<span className="text-blue-400 font-semibold">$</span>{' '}
|
|
<span className="text-zinc-300">curl -X POST https://your-bin.hatch.surf/test -d {'{\'hello\':\'world\'}'}</span>{'\n'}
|
|
<span className="text-emerald-400">✓ captured — view at https://your-bin.hatch.surf/inspect</span>
|
|
</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|