Simplify hero backgrounds and add entrance animations
- Replace animated blob/wave/noise backdrop with clean static gradient (radial ellipse highlights + linear fade) in hero-motion-backdrop - Remove all framer-motion from background layer; delete unused CSS classes: hero-blob, hero-sheet-glow, hero-sheet-wave, hero-sheet-noise, hero-title-shift keyframe, and responsive blob media queries - Add framer-motion stagger entrance to HeroContentMotion, HeroMotionItem, and HeroTitle (slide-up + fade on mount) - Refactor coming-soon page to use HeroContentMotion/HeroMotionItem instead of MotionFade (whileInView unreliable above the fold) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,14 @@ const sectionTypeOptions: PortfolioSectionType[] = [
|
||||
"LINK",
|
||||
];
|
||||
|
||||
const sectionTypeLabels: Record<PortfolioSectionType, string> = {
|
||||
RICH_TEXT: "Rich Text",
|
||||
GALLERY: "Gallery (image)",
|
||||
STATS: "Stats (text)",
|
||||
DELIVERABLES: "Deliverables (text)",
|
||||
LINK: "Link",
|
||||
};
|
||||
|
||||
const viewModeOptions: Array<{
|
||||
value: PortfolioProjectViewMode;
|
||||
label: string;
|
||||
@@ -137,7 +145,7 @@ const viewModeOptions: Array<{
|
||||
}> = [
|
||||
{ value: "GRID", label: "Grid", description: "Balanced modular layout.", icon: Grid2x2 },
|
||||
{ value: "STORY", label: "Story", description: "Narrative section flow.", icon: BookOpenText },
|
||||
{ value: "CASE_STUDY", label: "Case Study", description: "Structured challenge and result view.", icon: BriefcaseBusiness },
|
||||
{ value: "CASE_STUDY", label: "Case Study", description: "Structured challenge/solution/outcome view. Sections 1–3 become the lead panels.", icon: BriefcaseBusiness },
|
||||
];
|
||||
|
||||
const wizardSteps: Array<{
|
||||
@@ -1046,7 +1054,7 @@ export function PortfolioProjectForm({
|
||||
<SelectContent>
|
||||
{sectionTypeOptions.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
{sectionTypeLabels[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
type HeroMotionBackdropProps = {
|
||||
compact?: boolean;
|
||||
};
|
||||
@@ -8,13 +6,6 @@ export function HeroMotionBackdrop({}: HeroMotionBackdropProps) {
|
||||
return (
|
||||
<div className="hero-sheet pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div className="hero-sheet-base" />
|
||||
<div className="hero-blob hero-blob-left" />
|
||||
<div className="hero-blob hero-blob-right" />
|
||||
<div className="hero-blob hero-blob-bottom" />
|
||||
<div className="hero-sheet-glow" />
|
||||
<div className="hero-sheet-wave" />
|
||||
<div className="hero-sheet-noise" />
|
||||
|
||||
<div className="hero-sheet-veil" />
|
||||
<div className="hero-sheet-fade" />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HeroContentMotion, HeroMotionItem, HeroShell, HeroTitle, type HeroTitle
|
||||
|
||||
type PageHeroProps = {
|
||||
badge?: string;
|
||||
title: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
lines?: HeroTitleLineValue[];
|
||||
locale?: string;
|
||||
@@ -33,13 +33,19 @@ function getDefaultLines(title: string): HeroTitleLineValue[] {
|
||||
}
|
||||
|
||||
export function PageHero({ badge, title, description, lines, locale }: PageHeroProps) {
|
||||
const resolvedLines = lines ?? (title ? getDefaultLines(title) : undefined);
|
||||
|
||||
return (
|
||||
<HeroShell compactBackdrop variant="page">
|
||||
<HeroContentMotion variant="page">
|
||||
<HeroMotionItem variant="page">
|
||||
<HeroBadge>{badge ?? title}</HeroBadge>
|
||||
</HeroMotionItem>
|
||||
<HeroTitle lines={lines ?? getDefaultLines(title)} locale={locale} variant="page" />
|
||||
{(badge ?? title) ? (
|
||||
<HeroMotionItem variant="page">
|
||||
<HeroBadge>{badge ?? title}</HeroBadge>
|
||||
</HeroMotionItem>
|
||||
) : null}
|
||||
{resolvedLines ? (
|
||||
<HeroTitle lines={resolvedLines} locale={locale} variant="page" />
|
||||
) : null}
|
||||
{description ? (
|
||||
<HeroMotionItem variant="page">
|
||||
<p className="mx-auto mt-6 max-w-[34rem] text-base leading-7 text-foreground/72">
|
||||
|
||||
@@ -3,11 +3,28 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Container } from "@/components/layout/container";
|
||||
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const heroItemVariants = {
|
||||
hidden: { opacity: 0, y: 16 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.54, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
|
||||
const heroContainerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.13, delayChildren: 0.08 },
|
||||
},
|
||||
};
|
||||
|
||||
type HeroTone = "default" | "accent" | "soft";
|
||||
type HeroLineAlign = "start" | "center" | "end";
|
||||
|
||||
@@ -119,14 +136,27 @@ export function HeroContentMotion({
|
||||
children,
|
||||
className,
|
||||
}: HeroContentMotionProps) {
|
||||
return <div className={className}>{children}</div>;
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={heroContainerVariants}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroMotionItem({
|
||||
children,
|
||||
className,
|
||||
}: HeroContentMotionProps) {
|
||||
return <div className={className}>{children}</div>;
|
||||
return (
|
||||
<motion.div className={className} variants={heroItemVariants}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeroTitle({
|
||||
@@ -147,7 +177,7 @@ export function HeroTitle({
|
||||
);
|
||||
|
||||
return (
|
||||
<h1 className={titleClassName}>
|
||||
<motion.h1 className={titleClassName} variants={heroItemVariants}>
|
||||
{lines.map((line, index) => (
|
||||
<span
|
||||
key={`${index}-${line.text}`}
|
||||
@@ -172,7 +202,7 @@ export function HeroTitle({
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
</motion.h1>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ function PortfolioImage({
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
unoptimized
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
@@ -121,11 +120,7 @@ function SectionBlock({
|
||||
height={880}
|
||||
className="h-72 w-full rounded-surface object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-surface border border-dashed border-border px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
No image configured.
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -216,7 +211,7 @@ function AssetGallery({
|
||||
function BadgeCount({ count }: { count: number }) {
|
||||
return (
|
||||
<div className="rounded-pill border border-border/70 bg-background px-4 py-2 text-sm text-muted-foreground">
|
||||
{count}
|
||||
{count} {count === 1 ? "file" : "files"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user