// components/ui/apple-cards-carousel.tsx "use client"; import React, { useEffect, useRef, useState, createContext, useContext, } from "react"; import { IconArrowNarrowLeft, IconArrowNarrowRight, IconX, } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; import { AnimatePresence, motion } from "motion/react"; import Image, { ImageProps } from "next/image"; import { useOutsideClick } from "@/hooks/use-outside-click"; interface CarouselProps { items: JSX.Element[]; initialScroll?: number; } type Card = { src: string; title: string; category: string; content: React.ReactNode; }; export const CarouselContext = createContext<{ onCardClose: (index: number) => void; currentIndex: number; }>({ onCardClose: () => {}, currentIndex: 0, }); export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => { const carouselRef = React.useRef<HTMLDivElement>(null); const [canScrollL…
查看原始来源 ↗
Prompt
// components/ui/apple-cards-carousel.tsx
"use client";
import React, {
useEffect,
useRef,
useState,
createContext,
useContext,
} from "react";
import {
IconArrowNarrowLeft,
IconArrowNarrowRight,
IconX,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import Image, { ImageProps } from "next/image";
import { useOutsideClick } from "@/hooks/use-outside-click";
interface CarouselProps {
items: JSX.Element[];
initialScroll?: number;
}
type Card = {
src: string;
title: string;
category: string;
content: React.ReactNode;
};
export const CarouselContext = createContext<{
onCardClose: (index: number) => void;
currentIndex: number;
}>({
onCardClose: () => {},
currentIndex: 0,
});
export const Carousel = ({ items, initialScroll = 0 }: CarouselProps) => {
const carouselRef = React.useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = React.useState(false);
const [canScrollRight, setCanScrollRight] = React.useState(true);
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (carouselRef.current) {
carouselRef.current.scrollLeft = initialScroll;
checkScrollability();
}
}, [initialScroll]);
const checkScrollability = () => {
if (carouselRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth);
}
};
const scrollLeft = () => {
if (carouselRef.current) {
carouselRef.current.scrollBy({ left: -300, behavior: "smooth" });
}
};
const scrollRight = () => {
if (carouselRef.current) {
carouselRef.current.scrollBy({ left: 300, behavior: "smooth" });
}
};
const handleCardClose = (index: number) => {
if (carouselRef.current) {
const cardWidth = isMobile() ? 230 : 384; // (md:w-96)
const gap = isMobile() ? 4 : 8;
const scrollPosition = (cardWidth + gap) * (index + 1);
carouselRef.current.scrollTo({
left: scrollPosition,
behavior: "smooth",
});
setCurrentIndex(index);
}
};
const isMobile = () => {
return window && window.innerWidth < 768;
};
return (
<CarouselContext.Provider
value={{ onCardClose: handleCardClose, currentIndex }}
>
<div className="relative w-full">
<div
className="flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20"
ref={carouselRef}
onScroll={checkScrollability}
>
<div
className={cn(
"absolute right-0 z-[1000] h-auto w-[5%] overflow-hidden bg-gradient-to-l",
)}
></div>
<div
className={cn(
"flex flex-row justify-start gap-4 pl-4",
"mx-auto max-w-7xl", // remove max-w-4xl if you want the carousel to span the full width of its container
)}
>
{items.map((item, index) => (
<motion.div
initial={{
opacity: 0,
y: 20,
}}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.5,
delay: 0.2 * index,
ease: "easeOut",
},
}}
key={"card" + index}
className="rounded-3xl last:pr-[5%] md:last:pr-[33%]"
>
{item}
</motion.div>
))}
</div>
</div>
<div className="mr-10 flex justify-end gap-2">
<button
className="relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 disabled:opacity-50"
onClick={scrollLeft}
disabled={!canScrollLeft}
>
<IconArrowNarrowLeft className="h-6 w-6 text-gray-500" />
</button>
<button
className="relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 disabled:opacity-50"
onClick={scrollRight}
disabled={!canScrollRight}
>
<IconArrowNarrowRight className="h-6 w-6 text-gray-500" />
</button>
</div>
</div>
</CarouselContext.Provider>
);
};
export const Card = ({
card,
index,
layout = false,
}: {
card: Card;
index: number;
layout?: boolean;
}) => {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const { onCardClose, currentIndex } = useContext(CarouselContext);
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
handleClose();
}
}
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [open]);
useOutsideClick(containerRef, () => handleClose());
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
onCardClose(index);
};
return (
<>
<AnimatePresence>
{open && (
<div className="fixed inset-0 z-50 h-screen overflow-auto">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 h-full w-full bg-black/80 backdrop-blur-lg"
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={containerRef}
layoutId={layout ? `card-${card.title}` : undefined}
className="relative z-[60] mx-auto my-10 h-fit max-w-5xl rounded-3xl bg-white p-4 font-sans md:p-10 dark:bg-neutral-900"
>
<button
className="sticky top-4 right-0 ml-auto flex h-8 w-8 items-center justify-center rounded-full bg-black dark:bg-white"
onClick={handleClose}
>
<IconX className="h-6 w-6 text-neutral-100 dark:text-neutral-900" />
</button>
<motion.p
layoutId={layout ? `category-${card.title}` : undefined}
className="text-base font-medium text-black dark:text-white"
>
{card.category}
</motion.p>
<motion.p
layoutId={layout ? `title-${card.title}` : undefined}
className="mt-4 text-2xl font-semibold text-neutral-700 md:text-5xl dark:text-white"
>
{card.title}
</motion.p>
<div className="py-10">{card.content}</div>
</motion.div>
</div>
)}
</AnimatePresence>
<motion.button
layoutId={layout ? `card-${card.title}` : undefined}
onClick={handleOpen}
className="relative z-10 flex h-80 w-56 flex-col items-start justify-start overflow-hidden rounded-3xl bg-gray-100 md:h-[40rem] md:w-96 dark:bg-neutral-900"
>
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 h-full bg-gradient-to-b from-black/50 via-transparent to-transparent" />
<div className="relative z-40 p-8">
<motion.p
layoutId={layout ? `category-${card.category}` : undefined}
className="text-left font-sans text-sm font-medium text-white md:text-base"
>
{card.category}
</motion.p>
<motion.p
layoutId={layout ? `title-${card.title}` : undefined}
className="mt-2 max-w-xs text-left font-sans text-xl font-semibold [text-wrap:balance] text-white md:text-3xl"
>
{card.title}
</motion.p>
</div>
<BlurImage
src={card.src}
alt={card.title}
fill
className="absolute inset-0 z-10 object-cover"
/>
</motion.button>
</>
);
};
export const BlurImage = ({
height,
width,
src,
className,
alt,
...rest
}: ImageProps) => {
const [isLoading, setLoading] = useState(true);
return (
<img
className={cn(
"h-full w-full transition duration-300",
isLoading ? "blur-sm" : "blur-0",
className,
)}
onLoad={() => setLoading(false)}
src={src as string}
width={width}
height={height}
loading="lazy"
decoding="async"
blurDataURL={typeof src === "string" ? src : undefined}
alt={alt ? alt : "Background of a beautiful view"}
{...rest}
/>
);
};
// hooks/use-outside-click.tsx
import React, { useEffect } from "react";
export const useOutsideClick = (
ref: React.RefObject<HTMLDivElement>,
callback: Function
) => {
useEffect(() => {
const listener = (event: any) => {
// DO NOTHING if the element being clicked is the target element or their children
if (!ref.current || ref.current.contains(event.target)) {
return;
}
callback(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, callback]);
};复用方法
关键决定
观察最终结果里,页面信息层级、模块顺序和 CTA 是否一口气讲清楚 是否同时成立。
换到你的题材
把“目标结果 → 产品名称、目标用户、页面模块和主按钮 → 约束条件”抽成模板,后续可复用到同类任务。
容易翻车
围绕 模块顺序、价值主张写法与 CTA 位置 做单变量对照,并记录模型、成本和失败结果;重复成立后再沉淀为 Skill。
建议模型
创作者
Aceternity UI 长期扎根Aceternity UI,交付可运行的界面,《Parallax Hero Images》配了完整实现思路。
继续探索
@Oluwaphilemon1 使用 Claude Fable 5 · Three.js · Blender · GSAP完成的网页案例,包含公开结果、完整 Prompt 与原始来源。
提示语
Claude Fable 5 和 GPT-5.6 很强🥵🥵🥵 下面是我如何使用 Claude 构建的 👇 提示词:「设计一个机构网站,把服务以 3D 超市商品包装的形式呈现」 使用 Three.js 和 WebGL 在浏览器中实时渲染所有产品 使用 Blender 制作包装模型,然后导出为 GLTF 以加载到 Three.js 中 使用 GSAP 实现流畅的产品旋转和悬停交互 如果你想做一个让客户感觉像是在购买高级商品的作品集,请保存这条 🛒
// components/ui/3d-pin.tsx "use client"; import React, { useState } from "react"; import { motion } from "motion/react"; import { cn } from "@/lib/utils"; export const PinContainer = ({ children, title, href, className, containerClassName, }: { children: React.ReactNode; title?: string; href?: string; className?: string; containerClassName?: string; }) => { const [transform, setTransform] = useState( "translate(-50%,-50%) rotateX(0deg)" ); const onMouseEnter = () => { setTransform("translate(-50%,-50%) rotateX(40deg) scale(0.8)"); }; const onMouseLeave = () => { setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)"); }; return ( <a className={cn( "relative group/pin z-50 cursor-pointer", containerClassName )} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} href={href || "/"} > <div style={{ perspective: "1000px", transform: "rotateX(70deg) translateZ(0deg)", }} className="…
提示语
// components/ui/3d-pin.tsx "use client"; import React, { useState } from "react"; import { motion } from "motion/react"; import { cn } from "@/lib/utils"; export const PinContainer = ({ children, title, href, className, containerClassName…
.webp)
Build a single full-viewport hero section in React + TypeScript + Vite + Tailwind CSS, using `lucide-react` for icons. The component is a character-figurine carousel called "TOONHUB". **Fonts (load in `index.html` head):** ```html <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" /> ``` Body font: `'Inter', sans-serif`. Display font (huge ghost text + bottom-right link): `'Anton', sans-serif`. **Image data (4 items, exact URLs and colors):** ```ts const IMAGES = [ { src: 'https://fifth-gentle-45902158.figma.site/_components/v2/4de492f6d9cf8244ad5293233e5c6f52407d42fc/1.02464a56.png', bg: '#F4845F', panel: '#F79B7F' }, { src: 'https://fifth-gentle-45902158.figma.site/_components/v2/4d…
提示语
Build a single full-viewport hero section in React + TypeScript + Vite + Tailwind CSS, using `lucide-react` for icons. The component is a character-figurine carousel called "TOONHUB". **Fonts (load in `index.html` head):** ```html <link re…