alternative tangled frontend (extremely wip)

feat: abstract button

serenity f3cb8085 6f91a304

+94 -8
+75
src/components/Animated/Button.tsx
··· 1 + import { motion, Transition, Variants } from "motion/react"; 2 + import { MouseEventHandler, ReactNode, useState } from "react"; 3 + 4 + export const Button = ({ 5 + className, 6 + onClick, 7 + icon, 8 + iconVariants = { 9 + rest: { rotate: 0 }, 10 + active: { rotate: [0, -10, 10, -10, 10, 0, 0, 0] }, 11 + }, 12 + iconTransitions = { duration: 0.3, ease: "easeInOut" }, 13 + iconPosition = "left", 14 + iconClassName = "text-accent", 15 + label, 16 + labelClassName = "text-accent", 17 + underlineClassName = "bg-accent", 18 + disabled = false, 19 + }: { 20 + className?: string; 21 + onClick?: MouseEventHandler<HTMLButtonElement>; 22 + iconVariants?: Variants; 23 + iconTransitions?: Transition; 24 + iconClassName?: string; 25 + icon?: ReactNode; 26 + iconPosition?: "left" | "right"; 27 + labelClassName?: string; 28 + label: string; 29 + underlineClassName?: string; 30 + disabled?: boolean; 31 + }) => { 32 + const [isIconWiggle, setIsIconWiggle] = useState(false); 33 + 34 + const iconElement = icon && ( 35 + <motion.div 36 + variants={iconVariants} 37 + initial="rest" 38 + transition={iconTransitions} 39 + className={iconClassName} 40 + animate={isIconWiggle ? "active" : "rest"} 41 + > 42 + {icon} 43 + </motion.div> 44 + ); 45 + 46 + return ( 47 + <motion.button 48 + onClick={onClick} 49 + className={`flex items-center gap-2 pl-2 ${className}`} 50 + initial="initial" 51 + whileHover="hover" 52 + onHoverStart={() => setIsIconWiggle(true)} 53 + onAnimationComplete={() => setIsIconWiggle(false)} 54 + disabled={disabled} 55 + > 56 + {iconPosition === "left" && iconElement} 57 + <motion.div className="relative inline-block"> 58 + <p className={labelClassName}>{label}</p> 59 + <motion.span 60 + className={`absolute bottom-1 left-0 h-0.25 w-full origin-center ${underlineClassName}`} 61 + variants={{ 62 + initial: { scaleX: 0 }, 63 + hover: { scaleX: 1 }, 64 + }} 65 + transition={{ 66 + type: "spring", 67 + duration: 0.2, 68 + bounce: 0.3, 69 + }} 70 + /> 71 + </motion.div> 72 + {iconPosition === "right" && iconElement} 73 + </motion.button> 74 + ); 75 + };
+19 -8
src/components/Auth/SignOutButton.tsx
··· 1 + import { Button } from "@/components/Animated/Button"; 1 2 import { LucideLogOut } from "@/components/Icons/LucideLogOut"; 2 3 import { useOAuth } from "@/lib/oauth"; 3 - import { useNavigate, useRouter } from "@tanstack/react-router"; 4 + import { useNavigate } from "@tanstack/react-router"; 4 5 5 6 export const SignOutButton = () => { 6 7 const { signOut } = useOAuth(); 7 8 const navigate = useNavigate(); 8 - const router = useRouter(); 9 9 10 10 const handleSignOut = () => { 11 11 signOut(); 12 12 navigate({ to: "/" }); 13 13 }; 14 14 15 + const icon = <LucideLogOut />; 16 + 15 17 return ( 16 - <button 17 - className="flex cursor-pointer items-center gap-2 pl-2" 18 + <Button 19 + icon={icon} 20 + label="Sign Out" 21 + className="cursor-pointer" 22 + labelClassName="text-sm text-negative" 18 23 onClick={handleSignOut} 19 - > 20 - <LucideLogOut /> 21 - <p className="text-sm">Sign Out</p> 22 - </button> 24 + iconTransitions={{ duration: 0.2, ease: "easeInOut" }} 25 + iconVariants={{ 26 + active: { 27 + x: [0, 3, -3, 0], 28 + opacity: [1, 0, 0, 1], 29 + }, 30 + }} 31 + iconClassName="text-negative" 32 + underlineClassName="bg-negative" 33 + /> 23 34 ); 24 35 };