diff --git a/.gitignore b/.gitignore index b31bf87..a89f002 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ next-env.d.ts # ai-docs /planning -/docs/m2-plan \ No newline at end of file +/docs/m2-plan + +# claude code +.claude/ \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 9f276c8..0470235 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,6 +9,7 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import { cn, constructMetadata } from "@/lib/utils"; import { Toaster } from "@/components/ui/sonner"; import { WalletProvider } from "@/components/wallet/wallet-provider"; +import { DevTools } from "@/components/dev-tools"; interface RootLayoutProps { children: React.ReactNode; @@ -45,6 +46,7 @@ export default function RootLayout({ children }: RootLayoutProps) { {children} + diff --git a/components.json b/components.json index 3040da4..9898772 100644 --- a/components.json +++ b/components.json @@ -10,8 +10,12 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "radix", "aliases": { "components": "@/components", "utils": "@/lib/utils" + }, + "registries": { + "@lucide-animated": "https://lucide-animated.com/r/{name}.json" } -} \ No newline at end of file +} diff --git a/components/dev-tools.tsx b/components/dev-tools.tsx new file mode 100644 index 0000000..e252243 --- /dev/null +++ b/components/dev-tools.tsx @@ -0,0 +1,14 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const Agentation = + process.env.NODE_ENV === "development" + ? dynamic(() => import("agentation").then((mod) => mod.Agentation), { + ssr: false, + }) + : () => null; + +export function DevTools() { + return ; +} diff --git a/components/sections/connected-steps.tsx b/components/sections/connected-steps.tsx index 3f2378f..e0e4097 100644 --- a/components/sections/connected-steps.tsx +++ b/components/sections/connected-steps.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { motion } from "framer-motion"; +import Balancer from "react-wrap-balancer"; interface StepProps { number: number; @@ -24,9 +25,9 @@ const Step = ({ number, title, description }: StepProps) => ( {number}
-

{title}

+

{title}

- {description} + {description}

diff --git a/components/sections/dapp-builders.tsx b/components/sections/dapp-builders.tsx index 2347ec1..ada92c2 100644 --- a/components/sections/dapp-builders.tsx +++ b/components/sections/dapp-builders.tsx @@ -1,65 +1,107 @@ "use client"; import * as React from "react"; -import { - motion, - AnimatePresence, - useScroll, - useTransform, - useSpring, -} from "framer-motion"; +import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion"; +import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { ArrowRight, Copy } from "lucide-react"; +import { ArrowRight, Copy, Check } from "lucide-react"; +import Link from "next/link"; import Balancer from "react-wrap-balancer"; -import { AuroraBackground } from "@/components/layout/aurora-background"; import { cn } from "@/lib/utils"; import { StatusBadge } from "@/components/shared/status-badge"; -interface ComponentCard { +interface ComponentShowcase { id: string; title: string; + subtitle: string; code: string; - description: string; visual: React.ReactNode; } -const components: ComponentCard[] = [ +const components: ComponentShowcase[] = [ { - id: "chain-aware", - title: "Chain-Aware Forms", - code: '', - description: "Full validation + DID resolution support", + id: "account", + title: "Account Input", + subtitle: "SS58 validation, wallet connect, recent addresses", + code: '', visual: ( -
-
-
-
-
+
+
+
Destination
+
+
+
+
15oF4uVnB...R7xkwQ1
+
+
+
+ Valid +
+
+
+
Connected wallet
+
Address book
+
+
+ ), + }, + { + id: "balance", + title: "Balance Input", + subtitle: "Denomination switching, ED warnings, quick-fill", + code: '', + visual: ( +
+
+
Amount
-
-
+
+ 15.500 +
+
+
+ DOT +
+
+ {["25%", "50%", "75%", "Max"].map((pct) => ( +
{pct}
+ ))} +
+
+
+ Below existential deposit (1 DOT) +
), }, { - id: "smart-inputs", - title: "Smart Inputs", - code: '', - description: "Auto-formatting + unit conversion", + id: "vote", + title: "Governance Vote", + subtitle: "Standard, Split, SplitAbstain with conviction", + code: '', visual: ( -
-
-
-
-
-
-
-
-
+
+
+ {["Standard", "Split", "SplitAbstain"].map((mode, i) => ( +
{mode}
+ ))} +
+
+
Aye
+
Nay
+
+
+
+
+
Conviction
+
2x voting power
+
+
+
Lock period
+
~56 days
@@ -67,54 +109,84 @@ const components: ComponentCard[] = [ ), }, { - id: "decoding", - title: "Decoding Utilities", - code: "", - description: "Extrinsic tree analysis", + id: "call", + title: "Nested Call Builder", + subtitle: "Pallet/method cascade from on-chain metadata", + code: '', visual: ( -
-
+
+
+
Pallet
+
+ Balances + +
+
+
+
Method
+
+ transferKeepAlive + +
+
+
+
Auto-populated params
-
-
+ AccountId + dest
-
-
-
-
-
-
-
-
-
+
+ Balance + value
), }, { - id: "dedot", - title: "Dedot Type Integration", - code: "Input = findInputComponent(dedotTypeId)", - description: "Seamless integration with Dedot", + id: "bytes", + title: "Bytes Encoder", + subtitle: "Hex, Text, JSON, Base64, and File upload modes", + code: '', visual: ( -
-
-
-
-
-
-
-
-
-
-
+
+
+ {["Hex", "Text", "JSON", "Base64", "File"].map((mode, i) => ( +
{mode}
+ ))} +
+
+ 0x68656c6c6f20776f726c64 +
+
+ 11 bytes encoded + Text: "hello world" +
+
+ ), + }, + { + id: "enum", + title: "Enum Selector", + subtitle: "Metadata-driven variants with dynamic sub-fields", + code: '', + visual: ( +
+
+
MultiAddress
+
+ Id + AccountId32 +
-
-
-
-
-
+
+
+
+ AccountId32 +
+
+
+ 15oF4uVnB...R7xkwQ1
@@ -122,174 +194,246 @@ const components: ComponentCard[] = [ }, ]; -const benefits = [ - "Built-in error prevention", - "Real-time type checking", - "Auto-generated examples", -]; +const containerVariants = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { staggerChildren: 0.08 }, + }, +}; + +const itemVariants = { + hidden: { opacity: 0, y: 12 }, + show: { opacity: 1, y: 0 }, +}; export function DappBuildersSection() { - const containerRef = React.useRef(null); const sectionRef = React.useRef(null); const [activeIndex, setActiveIndex] = React.useState(0); const [copied, setCopied] = React.useState(false); - // Scroll configuration for the entire section - const { scrollYProgress: sectionProgress } = useScroll({ + const { scrollYProgress } = useScroll({ target: sectionRef, - offset: ["start start", "end start"], - }); - - // Scroll configuration for the card content - const { scrollYProgress: contentProgress } = useScroll({ - target: containerRef, - offset: ["start end", "end start"], + offset: ["start start", "end end"], }); - const springConfig = { stiffness: 300, damping: 30 }; - const smoothProgress = useSpring(contentProgress, springConfig); const progress = useTransform( - smoothProgress, - [0, 0.5], + scrollYProgress, + [0.08, 0.92], [0, components.length - 1] ); - // Handle scroll snapping React.useEffect(() => { const unsubscribe = progress.on("change", (latest) => { - setActiveIndex(Math.round(latest)); + const clamped = Math.max(0, Math.min(components.length - 1, Math.round(latest))); + setActiveIndex(clamped); }); return () => unsubscribe(); }, [progress]); + const handleCopy = async () => { + await navigator.clipboard.writeText(components[activeIndex].code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return (
-
-
- {" "} - {/* Update 1 */} -
-
- {" "} - {/* Update 2 */} - {/* Left Column - Scrolling Card */} -
-
- {/* Main Card */} - -
-
- +
+ {/* Ambient gradient */} +
+
+
+ +
+
+ + {/* Left Column — Header + Component Nav */} +
+ +
+ +
+

+ + Components that speak{" "} + + Substrate + + +

+

+ + Every input understands Polkadot's type system out of the box. + Validation, encoding, and edge cases handled -- so you ship + dApps, not workarounds. + +

+
+ + {/* Component list */} + + {components.map((component, index) => ( + setActiveIndex(index)} + className={cn( + "group flex w-full items-center gap-4 rounded-xl px-4 py-3 text-left transition-all duration-300", + activeIndex === index + ? "bg-gradient-to-r from-[#FF2670]/[0.06] to-[#7916F3]/[0.06] shadow-sm" + : "hover:bg-muted/60" + )} + > +
+ {String(index + 1).padStart(2, "0")}
-
- - -

+
+
+ {component.title} +
+
+ {component.subtitle} +
+
+
+ + ))} + + + + + + + +
+ + {/* Right Column — Live Preview Card */} +
+ {/* Glow behind card */} +
+ + + {/* Card header gradient line */} +
+ +
+ + + {/* Component title */} +
+
+

{components[activeIndex].title}

-
-
{components[activeIndex].code}
- -
-

- {components[activeIndex].description} +

+ {components[activeIndex].subtitle}

-
-
- {components[activeIndex].visual} -
-
- - -
- -
-
- {/* Right Column - Content */} -
- -
-
- -
-

- For dApp Builders -

-

- - Tired of context-switching between docs and code? Our - components come with everything you need to build fast. - -

-
+
+
+
+
+
+
+
-
- {benefits.map((benefit, index) => ( - -
- + {/* Code snippet */} +
+
+
+
+
+
+
+
- {benefit} - - ))} -
+
+                          {components[activeIndex].code}
+                        
+
-

- Stop fighting the chain – start using it. -

+ {/* Visual preview */} +
+
Preview
+ {components[activeIndex].visual} +
+ + +
+ - - + {/* Progress indicator */} +
+ {components.map((_, index) => ( +
diff --git a/components/sections/extrinsic-builder.tsx b/components/sections/extrinsic-builder.tsx index 28e4ea4..9ee1e79 100644 --- a/components/sections/extrinsic-builder.tsx +++ b/components/sections/extrinsic-builder.tsx @@ -4,56 +4,53 @@ import { motion, useScroll, useTransform } from "framer-motion"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { ArrowRight } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useRef } from "react"; +import { useRef, useEffect } from "react"; import { PolkadotIcon } from "@/components/icons/polkadot-icon"; import { TalismanIcon } from "@/components/icons/talisman-icon"; import { NovaWalletIcon } from "@/components/icons/nova-wallet-icon"; import { SubWalletIcon } from "@/components/icons/subwallet-icon"; import { RelaycodeIcon } from "@/components/icons/relaycode-logo"; -import { LoadingSkeleton } from "./loading-skeleton"; import { ConnectedSteps } from "./connected-steps"; import type React from "react"; -import { BlockIcon } from "../icons/block-icon"; +import { HandCoinsIcon } from "@/components/ui/hand-coins"; +import { LayersIcon } from "@/components/ui/layers"; +import { WorkflowIcon } from "@/components/ui/workflow"; +import { BoxIcon } from "@/components/ui/box"; +import { UsersIcon } from "@/components/ui/users"; +import { ZapIcon } from "@/components/ui/zap"; -const steps = [ - { - title: "Pick what you want to do", - description: "Choose from staking, NFTs, and more", - }, - { - title: "Fill human-friendly forms", - description: "No more cryptic parameters", - }, - { - title: "We handle the encoding", - description: "All the Polkadot magic, automated", - }, -]; +type IconHandle = { startAnimation: () => void; stopAnimation: () => void }; -const WalletIcon = ({ - children, - className, - angle, +function AutoAnimateIcon({ + Icon, + delay, + interval = 3000, + ...props }: { - children: React.ReactNode; + Icon: React.ForwardRefExoticComponent & Record>; + delay: number; + interval?: number; + size?: number; className?: string; - angle?: number; -}) => ( - - {children} - -); +}) { + const iconRef = useRef(null); + + useEffect(() => { + let intervalId: ReturnType; + const timeout = setTimeout(() => { + iconRef.current?.startAnimation(); + intervalId = setInterval(() => { + iconRef.current?.startAnimation(); + }, interval); + }, delay); + return () => { + clearTimeout(timeout); + clearInterval(intervalId); + }; + }, [delay, interval]); + + return ; +} const features = [ { @@ -62,9 +59,16 @@ const features = [ illustration: (
- {Array.from({ length: 6 }).map((_, i) => ( + {[ + { Icon: HandCoinsIcon, label: "Staking" }, + { Icon: LayersIcon, label: "Governance" }, + { Icon: WorkflowIcon, label: "XCM" }, + { Icon: BoxIcon, label: "Assets" }, + { Icon: UsersIcon, label: "Crowdloans" }, + { Icon: ZapIcon, label: "Utility" }, + ].map(({ Icon, label }, i) => ( - - + + + {label} { return ( -
+
@@ -418,7 +423,11 @@ const DemoPreview = () => {
- + Relaycode Extrinsic Builder
); }; @@ -474,7 +483,7 @@ export function ExtrinsicBuilderSection() {
-
+
@@ -52,13 +55,13 @@ export function HeroSection() { {/* Demo Section */}
{/* Ecosystem logos */} -
+
Web3 Foundation - Web3 Foundation + Web3 Foundation
- Polkadot + Polkadot Polkadot
@@ -71,14 +74,17 @@ export function HeroSection() {
-
-
- Relaycode Demo -
+
setIsPlaying(true)} + > + Relaycode Demo — bi-directional extrinsic builder + {!isPlaying && ( +
-
+ )}
diff --git a/components/sections/substrate-utilities.tsx b/components/sections/substrate-utilities.tsx index 4c84209..6a143fd 100644 --- a/components/sections/substrate-utilities.tsx +++ b/components/sections/substrate-utilities.tsx @@ -242,7 +242,8 @@ export function SubstrateUtilitiesSection() { diff --git a/components/sections/testimonials.tsx b/components/sections/testimonials.tsx index d4b21a6..829794e 100644 --- a/components/sections/testimonials.tsx +++ b/components/sections/testimonials.tsx @@ -22,26 +22,10 @@ const testimonials = [ ]; const logos = [ - { - src: "https://cdn.brandfetch.io/id6O2oGzv-/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B", - alt: "Google", - className: "h-8", - }, - { - src: "https://cdn.brandfetch.io/idwDWo4ONQ/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B", - alt: "Coinbase", - className: "h-6", - }, - { - src: "https://cdn.brandfetch.io/id-pjrLx_q/theme/dark/idKzmFfrAl.svg?c=1dxbfHSJFAPEGdCLU4o5B", - alt: "Binance", - className: "h-6", - }, - { - src: "https://cdn.brandfetch.io/idchmboHEZ/theme/dark/logo.svg?c=1dxbfHSJFAPEGdCLU4o5B", - alt: "Microsoft", - className: "h-6", - }, + { src: "/logos/w3f.svg", alt: "Web3 Foundation", label: "Web3 Foundation" }, + { src: "/logos/polkadot.svg", alt: "Polkadot", label: "Polkadot" }, + { src: "/logos/substrate.svg", alt: "Substrate", label: "Substrate" }, + { src: "/logos/dedot.png", alt: "Dedot", label: "Dedot", rounded: true }, ]; export function TestimonialsSection() { @@ -140,16 +124,14 @@ export function TestimonialsSection() { className="mt-24" >

- Used by teams at + Built for the Polkadot ecosystem

-
+
{logos.map((logo) => ( - {logo.alt} +
+ {logo.alt} + {logo.label} +
))}
diff --git a/components/sections/why-developers.tsx b/components/sections/why-developers.tsx index 5f5727d..7632a51 100644 --- a/components/sections/why-developers.tsx +++ b/components/sections/why-developers.tsx @@ -245,30 +245,26 @@ export function WhyDevelopers() { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.8, delay: 0.6 }} - className="mt-16 text-center" + className="mt-24 text-center" > -

Used by teams at

-
- Google - Coinbase - Binance - Microsoft +

Built for the Polkadot ecosystem

+
+
+ Web3 Foundation + Web3 Foundation +
+
+ Polkadot + Polkadot +
+
+ Substrate + Substrate +
+
+ Dedot + Dedot +
diff --git a/components/ui/box.tsx b/components/ui/box.tsx new file mode 100644 index 0000000..879d050 --- /dev/null +++ b/components/ui/box.tsx @@ -0,0 +1,117 @@ +"use client"; + +import type { Variants } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface BoxIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface BoxIconProps extends HTMLAttributes { + size?: number; +} + +const PATH_VARIANTS: Variants = { + normal: { + opacity: 1, + pathLength: 1, + transition: { + duration: 0.3, + opacity: { duration: 0.1 }, + }, + }, + animate: { + opacity: [0, 1], + pathLength: [0, 1], + transition: { + duration: 0.4, + opacity: { duration: 0.1 }, + }, + }, +}; + +const BoxIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + return { + startAnimation: () => controls.start("animate"), + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + controls.start("animate"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + +
+ ); + } +); + +BoxIcon.displayName = "BoxIcon"; + +export { BoxIcon }; diff --git a/components/ui/hand-coins.tsx b/components/ui/hand-coins.tsx new file mode 100644 index 0000000..88a82ff --- /dev/null +++ b/components/ui/hand-coins.tsx @@ -0,0 +1,150 @@ +"use client"; + +import type { Variants } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface HandCoinsIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface HandCoinsIconProps extends HTMLAttributes { + size?: number; +} + +const CIRCLE_VARIANTS: Variants = { + normal: { + translateY: 0, + opacity: 1, + transition: { + opacity: { duration: 0.2 }, + type: "spring", + stiffness: 150, + damping: 15, + bounce: 0.8, + }, + }, + animate: { + opacity: [0, 1], + translateY: [-20, 0], + transition: { + opacity: { duration: 0.2 }, + type: "spring", + stiffness: 150, + damping: 15, + bounce: 0.8, + }, + }, +}; + +const SECOND_CIRCLE_VARIANTS: Variants = { + normal: { + translateY: 0, + opacity: 1, + transition: { + opacity: { duration: 0.2 }, + delay: 0.15, + type: "spring", + stiffness: 150, + damping: 15, + bounce: 0.8, + }, + }, + animate: { + opacity: [0, 1], + translateY: [-20, 0], + transition: { + opacity: { duration: 0.2 }, + delay: 0.15, + type: "spring", + stiffness: 150, + damping: 15, + bounce: 0.8, + }, + }, +}; + +const HandCoinsIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start("animate"), + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + controls.start("animate"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + + + +
+ ); + } +); + +HandCoinsIcon.displayName = "HandCoinsIcon"; + +export { HandCoinsIcon }; diff --git a/components/ui/layers.tsx b/components/ui/layers.tsx new file mode 100644 index 0000000..56ad1e1 --- /dev/null +++ b/components/ui/layers.tsx @@ -0,0 +1,113 @@ +"use client"; + +import type { Transition } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface LayersIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface LayersIconProps extends HTMLAttributes { + size?: number; +} + +const DEFAULT_TRANSITION: Transition = { + type: "spring", + stiffness: 100, + damping: 14, + mass: 1, +}; + +const LayersIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: async () => { + await controls.start("firstState"); + await controls.start("secondState"); + }, + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + async (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + await controls.start("firstState"); + await controls.start("secondState"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + +
+ ); + } +); + +LayersIcon.displayName = "LayersIcon"; + +export { LayersIcon }; diff --git a/components/ui/users.tsx b/components/ui/users.tsx new file mode 100644 index 0000000..d194a6e --- /dev/null +++ b/components/ui/users.tsx @@ -0,0 +1,113 @@ +"use client"; + +import type { Variants } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface UsersIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface UsersIconProps extends HTMLAttributes { + size?: number; +} + +const PATH_VARIANTS: Variants = { + normal: { + translateX: 0, + transition: { + type: "spring", + stiffness: 200, + damping: 13, + }, + }, + animate: { + translateX: [-6, 0], + transition: { + delay: 0.1, + type: "spring", + stiffness: 200, + damping: 13, + }, + }, +}; + +const UsersIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start("animate"), + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + controls.start("animate"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + + +
+ ); + } +); + +UsersIcon.displayName = "UsersIcon"; + +export { UsersIcon }; diff --git a/components/ui/workflow.tsx b/components/ui/workflow.tsx new file mode 100644 index 0000000..0df18d7 --- /dev/null +++ b/components/ui/workflow.tsx @@ -0,0 +1,127 @@ +"use client"; + +import type { Transition, Variants } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface WorkflowIconHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface WorkflowIconProps extends HTMLAttributes { + size?: number; +} + +const TRANSITION: Transition = { + duration: 0.3, + opacity: { delay: 0.15 }, +}; + +const VARIANTS: Variants = { + normal: { + pathLength: 1, + opacity: 1, + }, + animate: (custom: number) => ({ + pathLength: [0, 1], + opacity: [0, 1], + transition: { + ...TRANSITION, + delay: 0.1 * custom, + }, + }), +}; + +const WorkflowIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start("animate"), + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + controls.start("animate"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + + + +
+ ); + } +); + +WorkflowIcon.displayName = "WorkflowIcon"; + +export { WorkflowIcon }; diff --git a/components/ui/zap.tsx b/components/ui/zap.tsx new file mode 100644 index 0000000..76c6b25 --- /dev/null +++ b/components/ui/zap.tsx @@ -0,0 +1,105 @@ +"use client"; + +import type { Variants } from "motion/react"; +import { motion, useAnimation } from "motion/react"; +import type { HTMLAttributes } from "react"; +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +export interface ZapHandle { + startAnimation: () => void; + stopAnimation: () => void; +} + +interface ZapProps extends HTMLAttributes { + size?: number; +} + +const PATH_VARIANTS: Variants = { + normal: { + opacity: 1, + pathLength: 1, + transition: { + duration: 0.6, + opacity: { duration: 0.1 }, + }, + }, + animate: { + opacity: [0, 1], + pathLength: [0, 1], + transition: { + duration: 0.6, + opacity: { duration: 0.1 }, + }, + }, +}; + +const ZapIcon = forwardRef( + ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + + useImperativeHandle(ref, () => { + isControlledRef.current = true; + + return { + startAnimation: () => controls.start("animate"), + stopAnimation: () => controls.start("normal"), + }; + }); + + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseEnter?.(e); + } else { + controls.start("animate"); + } + }, + [controls, onMouseEnter] + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + if (isControlledRef.current) { + onMouseLeave?.(e); + } else { + controls.start("normal"); + } + }, + [controls, onMouseLeave] + ); + + return ( +
+ + + +
+ ); + } +); + +ZapIcon.displayName = "ZapIcon"; + +export { ZapIcon }; diff --git a/package.json b/package.json index ac22b80..a8561ae 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "geist": "^1.3.1", "input-otp": "1.4.1", "lucide-react": "^0.454.0", + "motion": "^12.34.3", "ms": "^2.1.3", "next": "15.5.11", "next-themes": "^0.4.4", @@ -89,6 +90,7 @@ "@types/node": "^22", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", + "agentation": "^2.2.1", "eslint": "^8", "eslint-config-next": "15.5.11", "jest": "^29.7.0", diff --git a/public/logos/polkadot.svg b/public/logos/polkadot.svg new file mode 100644 index 0000000..4f378f0 --- /dev/null +++ b/public/logos/polkadot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/relaycode-demo-poster.png b/public/relaycode-demo-poster.png new file mode 100644 index 0000000..3b5b0e6 Binary files /dev/null and b/public/relaycode-demo-poster.png differ diff --git a/public/relaycode-demo.gif b/public/relaycode-demo.gif new file mode 100644 index 0000000..a3cfcf1 Binary files /dev/null and b/public/relaycode-demo.gif differ diff --git a/yarn.lock b/yarn.lock index 94b63c9..011eced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3457,6 +3457,11 @@ agent-base@6: dependencies: debug "4" +agentation@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/agentation/-/agentation-2.2.1.tgz#8a6f4bdb4dcae46cbbef49f563893f17446c9e57" + integrity sha512-yV9P1DggI7M3SRaRwLwt+xqE5lXqg5l8xtqCr8KzEkbnH8Wa6eRATU97uKnD7cC8FrsJP62Mmw0Xf5Xi5KV50Q== + ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -5213,6 +5218,15 @@ framer-motion@^11.3.31: motion-utils "^11.18.1" tslib "^2.4.0" +framer-motion@^12.34.3: + version "12.34.3" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.34.3.tgz#946f716bfef710d564bf721f4f364274f6278fd4" + integrity sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q== + dependencies: + motion-dom "^12.34.3" + motion-utils "^12.29.2" + tslib "^2.4.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -7421,11 +7435,31 @@ motion-dom@^11.18.1: dependencies: motion-utils "^11.18.1" +motion-dom@^12.34.3: + version "12.34.3" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.34.3.tgz#56224109a20bf2cb38277bfaedeeda5151ce369d" + integrity sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ== + dependencies: + motion-utils "^12.29.2" + motion-utils@^11.18.1: version "11.18.1" resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz" integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA== +motion-utils@^12.29.2: + version "12.29.2" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.29.2.tgz#8fdd28babe042c2456b078ab33b32daa3bf5938b" + integrity sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A== + +motion@^12.34.3: + version "12.34.3" + resolved "https://registry.yarnpkg.com/motion/-/motion-12.34.3.tgz#365a8ca9c0748e00ff3917b0f6b08a8aac90676c" + integrity sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw== + dependencies: + framer-motion "^12.34.3" + tslib "^2.4.0" + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"