ELF-457: add slot 3 rotating tagline above hero sub
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)
This commit is contained in:
parent
475fd95a59
commit
bcb4f1688e
@ -1,6 +1,10 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
/* Explicit source roots for Tailwind v4. Auto-detection misses files when
|
||||||
|
Turbopack's workspace root is ambiguous (e.g. shell cwd differs from the
|
||||||
|
web project root), so we pin the scan paths here. */
|
||||||
|
@source "../components/**/*.{ts,tsx}";
|
||||||
|
@source "../app/**/*.{ts,tsx}";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--surface-0: #09090b;
|
--surface-0: #09090b;
|
||||||
@ -17,11 +21,11 @@
|
|||||||
--code-border: #27272a;
|
--code-border: #27272a;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
/* Note: no `* { margin: 0; padding: 0; box-sizing: border-box; }` here.
|
||||||
margin: 0;
|
Tailwind v4's Preflight (included by `@import "tailwindcss"`) already
|
||||||
padding: 0;
|
resets margins on block elements and sets border-box globally. An
|
||||||
box-sizing: border-box;
|
unlayered `*` selector would beat Tailwind's `@layer utilities` and
|
||||||
}
|
silently break every margin utility (mb-*, mt-*, mx-*, etc.). */
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -37,14 +41,19 @@ body {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
/* Default link color. Wrapped in @layer base so Tailwind utilities
|
||||||
|
(text-white, text-zinc-*, etc.) in @layer utilities still win. An
|
||||||
|
unlayered `a { color: ... }` would silently override utilities. */
|
||||||
|
@layer base {
|
||||||
|
a {
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|||||||
@ -1,4 +1,32 @@
|
|||||||
|
"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() {
|
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 (
|
return (
|
||||||
<section className="pt-32 pb-20 px-6">
|
<section className="pt-32 pb-20 px-6">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
@ -11,6 +39,27 @@ export default function Hero() {
|
|||||||
inspector + mocker
|
inspector + mocker
|
||||||
</h1>
|
</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">
|
<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.
|
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">
|
<code className="mx-1 px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-base">
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
|
// Pin the Turbopack workspace root to this directory. Without this, Turbopack
|
||||||
|
// infers it from the shell's cwd, which during Paperclip agent runs is the
|
||||||
|
// paperclip project root — that misroutes CSS/source scanning and silently
|
||||||
|
// strips Tailwind utility classes from the production bundle.
|
||||||
|
turbopack: {
|
||||||
|
root: path.resolve(import.meta.dirname),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user