feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,67 +1,166 @@
|
||||
import * as React from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ButtonProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
variant?: 'primary' | 'outline';
|
||||
variant?: "primary" | "outline" | "ghost";
|
||||
size?: "normal" | "large";
|
||||
className?: string;
|
||||
showArrow?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium Button: Pill-shaped, binary-accent hover effect, unified design.
|
||||
*
|
||||
* On hover:
|
||||
* - A stream of binary characters scrolls across the button background
|
||||
* - Primary: white binary on dark bg
|
||||
* - Outline: blue binary on transparent bg
|
||||
* - Subtle, fast, and satisfying
|
||||
*/
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
href,
|
||||
children,
|
||||
variant = 'primary',
|
||||
variant = "primary",
|
||||
size = "normal",
|
||||
className = "",
|
||||
showArrow = true
|
||||
showArrow = true,
|
||||
}) => {
|
||||
const baseStyles = "inline-flex items-center gap-4 rounded-full font-bold uppercase tracking-widest transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] group";
|
||||
|
||||
const variants = {
|
||||
primary: "px-10 py-5 bg-slate-900 text-white hover:bg-slate-800 hover:-translate-y-1 hover:shadow-2xl hover:shadow-slate-900/20 text-sm",
|
||||
outline: "px-8 py-4 border border-slate-200 bg-white text-slate-900 hover:border-slate-400 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 text-sm"
|
||||
};
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
const [displayText, setDisplayText] = React.useState<string | null>(null);
|
||||
const contentRef = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{children}
|
||||
{showArrow && <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />}
|
||||
</>
|
||||
// Binary scramble on hover
|
||||
React.useEffect(() => {
|
||||
if (!hovered) {
|
||||
setDisplayText(null);
|
||||
return;
|
||||
}
|
||||
const original = contentRef.current?.textContent || "";
|
||||
if (!original) return;
|
||||
const chars = original.split("");
|
||||
const total = 20;
|
||||
let frame = 0;
|
||||
const iv = setInterval(() => {
|
||||
frame++;
|
||||
const s = chars.map((c, i) => {
|
||||
if (c === " ") return " ";
|
||||
const settle = (frame / total) * chars.length;
|
||||
if (i < settle) return c;
|
||||
return Math.random() > 0.5 ? "1" : "0";
|
||||
});
|
||||
setDisplayText(s.join(""));
|
||||
if (frame >= total) {
|
||||
setDisplayText(null);
|
||||
clearInterval(iv);
|
||||
}
|
||||
}, 1000 / 60);
|
||||
return () => clearInterval(iv);
|
||||
}, [hovered]);
|
||||
|
||||
const [binaryStr, setBinaryStr] = React.useState(
|
||||
"0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1",
|
||||
);
|
||||
|
||||
if (href.startsWith('#')) {
|
||||
React.useEffect(() => {
|
||||
setBinaryStr(
|
||||
Array.from({ length: 60 }, () => (Math.random() > 0.5 ? "0" : "1")).join(
|
||||
" ",
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const base =
|
||||
"relative inline-flex items-center justify-center gap-3 overflow-hidden rounded-full font-bold uppercase tracking-[0.15em] transition-all duration-300 group cursor-pointer";
|
||||
|
||||
const sizes: Record<string, string> = {
|
||||
normal: "px-8 py-4 text-[10px]",
|
||||
large: "px-10 py-5 text-[11px]",
|
||||
};
|
||||
|
||||
const variants: Record<string, string> = {
|
||||
primary:
|
||||
"bg-slate-900 text-white hover:shadow-xl hover:shadow-slate-900/20 hover:-translate-y-0.5",
|
||||
outline:
|
||||
"border border-slate-200 bg-transparent text-slate-900 hover:border-slate-400 hover:-translate-y-0.5",
|
||||
ghost: "bg-transparent text-slate-500 hover:text-slate-900",
|
||||
};
|
||||
|
||||
// Binary stream overlay colors by variant
|
||||
const binaryColor =
|
||||
variant === "primary"
|
||||
? "rgba(255,255,255,0.06)"
|
||||
: variant === "outline"
|
||||
? "rgba(59,130,246,0.08)"
|
||||
: "rgba(148,163,184,0.06)";
|
||||
|
||||
const inner = (
|
||||
<motion.span
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={`${base} ${sizes[size]} ${variants[variant]} ${className}`}
|
||||
>
|
||||
{/* Binary stream hover overlay */}
|
||||
<span
|
||||
className="absolute inset-0 flex items-center pointer-events-none overflow-hidden"
|
||||
style={{ opacity: hovered ? 1 : 0, transition: "opacity 0.3s ease" }}
|
||||
>
|
||||
<motion.span
|
||||
className="whitespace-nowrap font-mono text-[8px] tracking-[0.3em] select-none"
|
||||
style={{ color: binaryColor }}
|
||||
animate={hovered ? { x: [0, -200] } : { x: 0 }}
|
||||
transition={
|
||||
hovered ? { duration: 3, repeat: Infinity, ease: "linear" } : {}
|
||||
}
|
||||
>
|
||||
{binaryStr} {binaryStr}
|
||||
</motion.span>
|
||||
</span>
|
||||
|
||||
{/* Shimmer line on hover (top edge) */}
|
||||
<span
|
||||
className="absolute top-0 left-0 right-0 h-px pointer-events-none"
|
||||
style={{
|
||||
opacity: hovered ? 1 : 0,
|
||||
transition: "opacity 0.5s ease",
|
||||
background:
|
||||
variant === "primary"
|
||||
? "linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)"
|
||||
: "linear-gradient(90deg, transparent, rgba(59,130,246,0.15), transparent)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<span
|
||||
className="relative z-10 flex items-center gap-3"
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
<span ref={contentRef}>{displayText ?? children}</span>
|
||||
{showArrow && (
|
||||
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
)}
|
||||
</span>
|
||||
</motion.span>
|
||||
);
|
||||
|
||||
if (href.startsWith("#")) {
|
||||
return (
|
||||
<a href={href} className={`${baseStyles} ${variants[variant]} ${className}`}>
|
||||
{content}
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.querySelector(href)?.scrollIntoView({ behavior: "smooth" });
|
||||
}}
|
||||
>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href} className={`${baseStyles} ${variants[variant]} ${className}`}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const MotionButton: React.FC<ButtonProps> = ({
|
||||
href,
|
||||
children,
|
||||
variant = 'primary',
|
||||
className = "",
|
||||
showArrow = true
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button href={href} variant={variant} className={className} showArrow={showArrow}>
|
||||
{children}
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
return <Link href={href}>{inner}</Link>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user