fix(web): improve landing page motion and feedback

This commit is contained in:
Amruth Pillai
2026-10-01 07:36:40 +02:00
parent bc92b8cf7f
commit d9c121daaa
7 changed files with 37 additions and 16 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import { Link } from "@tanstack/react-router";
import { useRef } from "react";
import { Icon } from "@reactive-resume/ui/components/icon";
import { cn } from "@reactive-resume/utils/style";
import { SCENE, useLanding } from "./scroll";
import { prefersReducedMotion, SCENE, useLanding } from "./scroll";
import { designSystemBullet, ProfileSummary, Sheet } from "./sheet";
import { Doodle, labelClass, SceneCaption } from "./ui";
@@ -97,7 +97,7 @@ export function Check() {
// The pointer steers the lens while it's over the page; on leaving, the scroll engine takes it back.
const moveLens = (event: PointerEvent<HTMLDivElement>) => {
const section = sectionRef.current;
if (!section || event.pointerType !== "mouse") return;
if (!section || event.pointerType !== "mouse" || prefersReducedMotion()) return;
const rect = event.currentTarget.getBoundingClientRect();
section.dataset.lensHover = "";
section.style.setProperty("--lx", `${(((event.clientX - rect.left) / rect.width) * 100).toFixed(2)}%`);
+2 -2
View File
@@ -168,7 +168,7 @@ export function Design() {
<div
aria-hidden="true"
className="absolute top-[calc(var(--dpt)-var(--z)*4vh)] left-1/2 grid [transform:translate(-50%,-50%)_scale(calc(1-var(--z)*(1-var(--smin,.5))))] grid-cols-[repeat(5,var(--pw))] gap-[calc(var(--pw)*.12)] opacity-(--z) will-change-[transform,opacity] [transition:transform_.35s_var(--ease)]"
className="absolute top-(--dpt) left-1/2 grid [transform:translate(-50%,calc(-50%-var(--z)*4vh))_scale(calc(1-var(--z)*(1-var(--smin,.5))))] grid-cols-[repeat(5,var(--pw))] gap-[calc(var(--pw)*.12)] opacity-(--z) will-change-[transform,opacity] [transition:transform_.35s_var(--ease)]"
>
{minis.map((mini) => (
<div
@@ -187,7 +187,7 @@ export function Design() {
<div
data-stack
className="absolute top-[calc(var(--dpt)-var(--z)*4vh)] left-1/2 aspect-[612/792] w-(--pw) [transform:translate(-50%,-50%)_scale(calc(1-var(--z)*(1-var(--smin,.5))))] will-change-transform [transition:transform_.35s_var(--ease)]"
className="absolute top-(--dpt) left-1/2 aspect-[612/792] w-(--pw) [transform:translate(-50%,calc(-50%-var(--z)*4vh))_scale(calc(1-var(--z)*(1-var(--smin,.5))))] will-change-transform [transition:transform_.35s_var(--ease)]"
>
<div className="absolute inset-0 rounded-[2px] shadow-paper" />
{sheetTemplates.map((template, index) => (
@@ -16,6 +16,16 @@
--animate-mote: landing-mote var(--mote-duration) linear var(--mote-delay) infinite;
--animate-swing: landing-swing 7s ease-in-out 2s infinite;
--animate-flicker: landing-flicker 0.6s ease-out 1;
--animate-language-turn: landing-language-turn 0.9s var(--ease-in-out-strong) backwards;
@keyframes landing-language-turn {
from {
transform: translateY(0);
}
to {
transform: translateY(-1.25em);
}
}
@keyframes landing-breathe {
from {
+4 -1
View File
@@ -24,6 +24,7 @@ export const SCENE = {
*/
type LandingState = {
activeScene: number;
reducedMotion: boolean;
scrolled: boolean;
/** Write: 0 while the line is typed, 1 while the assistant's comment is up, 2 once the suggestion is accepted. */
writeStep: 0 | 1 | 2;
@@ -45,6 +46,7 @@ type LandingState = {
export const useLanding = create<LandingState>()(() => ({
activeScene: SCENE.hero,
reducedMotion: false,
scrolled: false,
writeStep: 0,
designStep: 0,
@@ -171,7 +173,7 @@ export function useScrollScenes(root: RefObject<HTMLElement | null>) {
// Check's lens follows a Lissajous path down the page, unless the pointer is steering it.
const checkSection = sectionAt(SCENE.check);
if (checkSection && checkSection.dataset.lensHover === undefined) {
if (checkSection && (reduced || checkSection.dataset.lensHover === undefined)) {
checkSection.style.setProperty("--lx", `${(50 + 30 * Math.sin(scan * Math.PI * 2.5)).toFixed(2)}%`);
checkSection.style.setProperty("--ly", `${(14 + 72 * scan).toFixed(2)}%`);
}
@@ -187,6 +189,7 @@ export function useScrollScenes(root: RefObject<HTMLElement | null>) {
const state = useLanding.getState();
const next: Partial<LandingState> = {
activeScene,
reducedMotion: reduced,
scrolled: window.scrollY > 8,
writeStep: write1 >= 0.84 ? 2 : write1 >= 0.4 ? 1 : 0,
designStep: stepsPassed(design, [0.15, 0.31, 0.47, 0.63]),
+14 -9
View File
@@ -130,15 +130,17 @@ const words = [
/** 07 Languages. The word turns over every 1.9s while the section is near; it can be paused. */
export function Languages() {
const near = useLanding((state) => state.activeScene >= SCENE.numbers && state.activeScene <= SCENE.support);
const reducedMotion = useLanding((state) => state.reducedMotion);
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const current = words[index] ?? words[0];
const current = words[index % words.length] ?? words[0];
const previous = words[(index - 1) % words.length];
useEffect(() => {
if (!near || paused || prefersReducedMotion()) return;
const timer = window.setInterval(() => setIndex((value) => (value + 1) % words.length), 1900);
const timer = window.setInterval(() => setIndex((value) => value + 1), 1900);
return () => window.clearInterval(timer);
}, [near, paused]);
}, [near, paused, reducedMotion]);
return (
<section
@@ -159,14 +161,17 @@ export function Languages() {
className="font-anybody h-[1.25em] w-full overflow-hidden text-[clamp(48px,7.5vw,140px)] leading-[1.25] font-light tracking-[-.03em] text-ink"
>
<div
className="transition-transform duration-[.9s] ease-[cubic-bezier(.7,0,.2,1)]"
style={{ transform: `translateY(${index * -1.25}em)` }}
key={index}
className={cn(index > 0 && "motion-safe:animate-language-turn [transform:translateY(-1.25em)]")}
>
{words.map((item) => (
<div key={item.lang} lang={item.lang} dir={item.dir} className="h-[1.25em] whitespace-nowrap">
{item.word}
{previous && (
<div lang={previous.lang} dir={previous.dir} className="h-[1.25em] whitespace-nowrap">
{previous.word}
</div>
))}
)}
<div lang={current?.lang} dir={current?.dir} className="h-[1.25em] whitespace-nowrap">
{current?.word}
</div>
</div>
</div>
<div className="flex items-center gap-2">
+4 -1
View File
@@ -189,7 +189,10 @@ export function Share() {
<button
type="button"
onClick={copy}
className="flex h-11 items-center gap-1.5 rounded-full bg-(--night-ink) px-[18px] font-ui text-sm font-semibold text-(--night-on-ink)"
className={cn(
"flex h-11 items-center gap-1.5 rounded-full px-[18px] font-ui text-sm font-semibold text-(--night-on-ink) transition-colors duration-standard",
copied ? "bg-(--night-accent)" : "bg-(--night-ink)",
)}
>
<Icon name={copied ? "check" : "link"} size={18} />
{copied ? t`Copied` : t`Copy link`}
+1 -1
View File
@@ -17,7 +17,7 @@ export function CtaLink({ size, className }: CtaLinkProps) {
<Link
to="/dashboard"
className={cn(
"inline-flex shrink-0 items-center rounded-full bg-accent font-ui font-semibold text-on-accent transition-colors hover:bg-accent-hover",
"inline-flex shrink-0 items-center rounded-full bg-accent font-ui font-semibold text-on-accent transition-[background-color,transform] duration-[160ms] ease-out-strong hover:bg-accent-hover motion-safe:active:[transform:scale(.97)]",
size === "header" && "h-[38px] px-4 text-sm",
size === "hero" && "h-[54px] gap-2.5 px-6 text-base shadow-e2",
size === "closing" && "h-14 gap-2.5 px-7 text-[17px] shadow-e2",