feat: overdrive landing page — live chat animation + scroll-driven reveals
A) Live App Preview: - Chat messages animate in with staggered timing (0.6s apart) - Typing indicator with bouncing dots appears before AI response, then fades out as the response lines arrive - Sidebar items stagger in during the entrance sequence - Creates a "show don't tell" demo moment in the hero B) Scroll-Driven Enhancements (@supports animation-timeline): - Sections use CSS scroll-driven animations instead of JS IntersectionObserver - Problem cards, feature cards, pricing cards, and step cards stagger within their parent as they enter the viewport - Social proof bar has subtle parallax drift - Falls back to existing JS-based reveal for Firefox/older browsers Accessibility: - prefers-reduced-motion removes all chat animations, shows content immediately, hides typing indicator entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -216,25 +216,40 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="landing-preview-canvas">
|
<div className="landing-preview-canvas">
|
||||||
<div className="landing-mock-session">
|
<div className="landing-mock-session">
|
||||||
<div className="landing-mock-chat-line">
|
<div className="landing-chat-animated" style={{ '--chat-index': 0 } as React.CSSProperties}>
|
||||||
<span className="label">You:</span>
|
<div className="landing-mock-chat-line">
|
||||||
<span className="text">User can't access shared drive after password reset</span>
|
<span className="label">You:</span>
|
||||||
|
<span className="text">User can't access shared drive after password reset</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="landing-mock-chat-line">
|
<div className="landing-chat-animated" style={{ '--chat-index': 1 } as React.CSSProperties}>
|
||||||
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
<div className="landing-typing-indicator">
|
||||||
<span className="text">This is likely a cached credential issue. Let's check a few things:</span>
|
<span /><span /><span />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="landing-mock-chat-line">
|
<div className="landing-chat-animated" style={{ '--chat-index': 2 } as React.CSSProperties}>
|
||||||
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
<div className="landing-mock-chat-line">
|
||||||
<span className="text">1. Run <code>klist purge</code> to clear Kerberos tickets</span>
|
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
||||||
|
<span className="text">This is likely a cached credential issue. Let's check a few things:</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="landing-mock-chat-line">
|
<div className="landing-chat-animated" style={{ '--chat-index': 3 } as React.CSSProperties}>
|
||||||
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
<div className="landing-mock-chat-line">
|
||||||
<span className="text">2. Open Credential Manager → remove saved entries for the share</span>
|
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
||||||
|
<span className="text">1. Run <code>klist purge</code> to clear Kerberos tickets</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="landing-mock-chat-line doc">
|
<div className="landing-chat-animated" style={{ '--chat-index': 4 } as React.CSSProperties}>
|
||||||
<span className="label">Auto-doc:</span>
|
<div className="landing-mock-chat-line">
|
||||||
<span className="text">3 steps captured ✓</span>
|
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
|
||||||
|
<span className="text">2. Open Credential Manager → remove saved entries for the share</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="landing-chat-animated" style={{ '--chat-index': 5 } as React.CSSProperties}>
|
||||||
|
<div className="landing-mock-chat-line doc">
|
||||||
|
<span className="label">Auto-doc:</span>
|
||||||
|
<span className="text">3 steps captured ✓</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1503,4 +1503,214 @@
|
|||||||
.landing-cta-email-form {
|
.landing-cta-email-form {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
OVERDRIVE: Live Chat Animation + Scroll Enhancements
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* ── A) ANIMATED CHAT MESSAGES ── */
|
||||||
|
/* Each chat line fades in and slides up with staggered timing.
|
||||||
|
The typing indicator appears between user message and AI response,
|
||||||
|
then hides when the AI lines appear. */
|
||||||
|
|
||||||
|
.landing-chat-animated {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
animation: landingChatReveal 0.4s ease-out both;
|
||||||
|
/* Base delay: 1.5s (after hero entrance) + stagger per line */
|
||||||
|
animation-delay: calc(1.5s + var(--chat-index, 0) * 0.6s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The typing indicator (index 1) appears, then fades out when index 2 appears */
|
||||||
|
.landing-chat-animated:nth-child(2) {
|
||||||
|
animation: landingTypingLifecycle 1.8s ease both;
|
||||||
|
animation-delay: calc(1.5s + 0.6s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes landingChatReveal {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes landingTypingLifecycle {
|
||||||
|
0% { opacity: 0; transform: translateY(8px); }
|
||||||
|
15% { opacity: 1; transform: translateY(0); }
|
||||||
|
70% { opacity: 1; transform: translateY(0); }
|
||||||
|
100% { opacity: 0; transform: translateY(0); height: 0; padding: 0; margin: 0; overflow: hidden; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typing indicator dots ── */
|
||||||
|
.landing-typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-typing-indicator span {
|
||||||
|
display: block;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #60a5fa;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: landingTypingBounce 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes landingTypingBounce {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar items stagger in ── */
|
||||||
|
.landing-preview-sidebar-item {
|
||||||
|
opacity: 0;
|
||||||
|
animation: landingChatReveal 0.3s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-preview-sidebar-item:nth-child(1) { animation-delay: 0.8s; }
|
||||||
|
.landing-preview-sidebar-item:nth-child(2) { animation-delay: 0.9s; }
|
||||||
|
.landing-preview-sidebar-item:nth-child(3) { animation-delay: 1.0s; }
|
||||||
|
.landing-preview-sidebar-item:nth-child(4) { animation-delay: 1.1s; }
|
||||||
|
.landing-preview-sidebar-item:nth-child(5) { animation-delay: 1.2s; }
|
||||||
|
|
||||||
|
/* ── B) SCROLL-DRIVEN ENHANCEMENTS ── */
|
||||||
|
/* Upgrade the basic reveal to use scroll-driven parallax where supported */
|
||||||
|
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
/* Sections get a subtle parallax lift as they enter viewport */
|
||||||
|
.landing-reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
animation: landingScrollReveal linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 40%;
|
||||||
|
/* Override the JS-based transition approach */
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No need for the JS .visible class when scroll-driven is available */
|
||||||
|
.landing-reveal.visible {
|
||||||
|
/* Intentionally blank — scroll-driven animation handles it */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes landingScrollReveal {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem cards stagger within their parent */
|
||||||
|
.landing-problem-card {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: landingScrollReveal linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-problem-card:nth-child(1) { animation-range: entry 0% entry 40%; }
|
||||||
|
.landing-problem-card:nth-child(2) { animation-range: entry 5% entry 45%; }
|
||||||
|
.landing-problem-card:nth-child(3) { animation-range: entry 10% entry 50%; }
|
||||||
|
.landing-problem-card:nth-child(4) { animation-range: entry 15% entry 55%; }
|
||||||
|
|
||||||
|
/* Feature cards stagger */
|
||||||
|
.landing-feature-card {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: landingScrollReveal linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-feature-card:nth-child(1) { animation-range: entry 0% entry 35%; }
|
||||||
|
.landing-feature-card:nth-child(2) { animation-range: entry 3% entry 38%; }
|
||||||
|
.landing-feature-card:nth-child(3) { animation-range: entry 6% entry 41%; }
|
||||||
|
.landing-feature-card:nth-child(4) { animation-range: entry 9% entry 44%; }
|
||||||
|
.landing-feature-card:nth-child(5) { animation-range: entry 12% entry 47%; }
|
||||||
|
.landing-feature-card:nth-child(6) { animation-range: entry 15% entry 50%; }
|
||||||
|
|
||||||
|
/* Pricing cards stagger */
|
||||||
|
.landing-pricing-card {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: landingScrollReveal linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-pricing-card:nth-child(1) { animation-range: entry 0% entry 35%; }
|
||||||
|
.landing-pricing-card:nth-child(2) { animation-range: entry 5% entry 40%; }
|
||||||
|
.landing-pricing-card:nth-child(3) { animation-range: entry 10% entry 45%; }
|
||||||
|
|
||||||
|
/* How it works steps stagger */
|
||||||
|
.landing-step-card {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation: landingScrollReveal linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-step-card:nth-child(1) { animation-range: entry 0% entry 35%; }
|
||||||
|
.landing-step-card:nth-child(2) { animation-range: entry 5% entry 40%; }
|
||||||
|
.landing-step-card:nth-child(3) { animation-range: entry 10% entry 45%; }
|
||||||
|
|
||||||
|
/* Social proof bar parallax — slight upward drift */
|
||||||
|
.landing-social-proof-bar {
|
||||||
|
animation: landingParallaxUp linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% exit 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes landingParallaxUp {
|
||||||
|
from { transform: translateY(12px); }
|
||||||
|
to { transform: translateY(-12px); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── REDUCED MOTION ── */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.landing-chat-animated,
|
||||||
|
.landing-preview-sidebar-item {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-typing-indicator span {
|
||||||
|
animation: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide typing indicator entirely in reduced motion */
|
||||||
|
.landing-chat-animated:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user