mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
fix(web): improve landing page motion and feedback
This commit is contained in:
@@ -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)}%`);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user