Add ToDo-Karten product page, fluid design tokens, and Navbar/Hero fixes
New /todo-cards page (Hero, How-it-works, Focus, Testimonials, Pricing) built with the fluid clamp() token system (app/lib/fluid.ts) and scroll-reveal animations (app/components/Reveal.tsx), matching styling consistency with /challenge's testimonial section. Navbar: page-aware active state and anchor-link navigation from any route (not just "/"), fixed logo click to actually navigate instead of silently rewriting the URL, fluid nav text size, custom hash-scroll handling for cross-page anchor links. Hero: responsive breakpoint fix for the text/image split at Tablet widths, entrance animation for the brand's orange dot, removed a red dot artifact from hero.png. Also includes prior uncommitted work on About/Blog/Newsletter/Footer and the cart lib (app/lib/cart.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { motion, type Variants } from "motion/react";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
|
||||
const fadeUp: Variants = {
|
||||
hidden: { opacity: 0, y: 28 },
|
||||
show: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } },
|
||||
};
|
||||
|
||||
type RevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
/** Delay in seconds, e.g. for staggering siblings that aren't inside a RevealGroup. */
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
/** Fades a section up into place once, the first time it scrolls into view. */
|
||||
export function Reveal({ children, className, style, delay = 0 }: RevealProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
style={style}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
variants={fadeUp}
|
||||
transition={{ delay }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const staggerContainer: Variants = {
|
||||
hidden: {},
|
||||
show: { transition: { staggerChildren: 0.12 } },
|
||||
};
|
||||
|
||||
/** Wraps a grid/row of cards — reveals children one-by-one as it scrolls into view. */
|
||||
export function RevealGroup({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
variants={staggerContainer}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Child item for use inside a RevealGroup — same fade-up motion, driven by the parent's stagger. */
|
||||
export function RevealItem({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<motion.div className={className} variants={fadeUp}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const popIn: Variants = {
|
||||
hidden: { x: 140, y: -70, scale: 0.5, opacity: 0 },
|
||||
show: {
|
||||
// x is a single continuous glide (140 → 0), deliberately NOT
|
||||
// keyframed in step with y's bounce schedule. It used to share y's
|
||||
// per-segment alternating ease so it would decelerate to a dead stop
|
||||
// at every bounce peak alongside y — physically correct for y (a
|
||||
// ball's vertical speed really is zero at the top of each arc) but
|
||||
// wrong for x, which has no reason to pause horizontally just because
|
||||
// it's momentarily at peak height. That shared stop-start was read as
|
||||
// "stops unnaturally in between". Giving x its own transition (below)
|
||||
// decouples it completely, so it glides smoothly the whole time while
|
||||
// y still bounces underneath it.
|
||||
x: 0,
|
||||
y: [-70, 0, -28, 0, -12, 0, -3, 0],
|
||||
scale: [0.5, 1.05, 0.95, 1.03, 0.97, 1.02, 0.99, 1],
|
||||
// Array, not a bare 1 — a scalar target here would fade opacity
|
||||
// linearly across the *entire* 1s duration against the shared `times`
|
||||
// grid below, so the dot would still be half-transparent through its
|
||||
// first, fastest bounces. Snap to fully opaque at the first keyframe
|
||||
// instead, matching how a real solid ball would look immediately.
|
||||
opacity: [0, 1, 1, 1, 1, 1, 1, 1],
|
||||
transition: {
|
||||
// Applies to y/scale/opacity (anything without its own override
|
||||
// below) — the actual bounce schedule.
|
||||
default: {
|
||||
duration: 1,
|
||||
times: [0, 0.22, 0.4, 0.55, 0.68, 0.8, 0.9, 1],
|
||||
// Per-segment, not a single ease for the whole animation — a real
|
||||
// bounce accelerates while falling (gravity) and decelerates
|
||||
// while rising toward each peak. Alternating fall (easeIn) / rise
|
||||
// (easeOut), one entry per keyframe-to-keyframe transition.
|
||||
ease: ["easeIn", "easeOut", "easeIn", "easeOut", "easeIn", "easeOut", "easeIn"],
|
||||
},
|
||||
// x's own transition — one smooth decelerating tween across the
|
||||
// full duration, independent of the y bounce's stop-start rhythm.
|
||||
x: { duration: 1, ease: "easeOut" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* One-shot "bouncing ball" drop-in for a small accent glyph (e.g. the
|
||||
* brand's orange dot) — arrives from the top right and bounces to rest,
|
||||
* not a looping pulse. `motion.span` needs a Client Component, same reason
|
||||
* Reveal itself exists: keep "use client" isolated here so callers like
|
||||
* Hero.tsx can stay Server Components.
|
||||
*/
|
||||
export function PopIn({ children, className, delay = 0 }: RevealProps) {
|
||||
return (
|
||||
<motion.span
|
||||
className={className}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-80px" }}
|
||||
variants={popIn}
|
||||
transition={{ delay }}
|
||||
>
|
||||
{children}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user