feat(ui): optimize CTA buttons with contextual copy and fix static export build issues

CTA Optimization:

- Implement scenario-based CTA text across 6 key locations

- Add responsive Header CTA with icon+compact design

- Enhance CTA Section with vision-driven copy

Bug Fixes:

- Fix useSearchParams() build failure with dynamic import wrapper

- Remove useSearchParams() from PageTransition component

- Fix React 19 useEffect lint errors via useSyncExternalStore

UI Enhancements:

- Add ripple effects and gradient animations to Button

- Enhance loading skeleton with branded pulse animation
This commit is contained in:
张翔
2026-05-11 19:03:37 +08:00
parent c474394237
commit f08874f5c4
20 changed files with 794 additions and 112 deletions
+40 -10
View File
@@ -3,26 +3,25 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-white min-h-[44px] min-w-[44px] touch-manipulation",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-white min-h-[44px] min-w-[44px] touch-manipulation relative overflow-hidden",
{
variants: {
variant: {
default:
"bg-[var(--color-brand-primary)] text-white hover:bg-[var(--color-brand-primary-hover)] hover:shadow-[0_4px_12px_rgba(var(--color-brand-primary-rgb),0.25)] hover:-translate-y-0.5 active:scale-[0.98]",
"bg-[var(--color-brand-primary)] text-white hover:bg-[var(--color-brand-primary-hover)] hover:shadow-[0_4px_16px_rgba(var(--color-brand-primary-rgb),0.3)] hover:-translate-y-0.5 active:scale-[0.97] group/btn before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:-translate-x-full hover:before:translate-x-full before:transition-transform before:duration-700 before:ease-out",
secondary:
"bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)] hover:shadow-[0_4px_12px_rgba(var(--color-primary-rgb),0.25)] hover:-translate-y-0.5 active:scale-[0.98]",
"bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)] hover:shadow-[0_4px_16px_rgba(var(--color-primary-rgb),0.25)] hover:-translate-y-0.5 active:scale-[0.97]",
destructive:
"bg-[var(--color-brand-primary)] text-white hover:bg-[var(--color-brand-primary-hover)] focus-visible:ring-[var(--color-brand-primary)]",
"bg-[var(--color-brand-primary)] text-white hover:bg-[var(--color-brand-primary-hover)] focus-visible:ring-[var(--color-brand-primary)] active:scale-[0.97]",
outline:
"border-2 border-[var(--color-primary)] bg-transparent text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white hover:-translate-y-0.5 active:scale-[0.98]",
"border-2 border-[var(--color-primary)] bg-transparent text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white hover:-translate-y-0.5 hover:border-[var(--color-brand-primary)] active:scale-[0.97] hover:shadow-[0_4px_12px_rgba(var(--color-brand-primary-rgb),0.15)]",
ghost:
"text-[var(--color-primary-light)] hover:bg-[var(--color-primary-lighter)] hover:text-[var(--color-primary)]",
"text-[var(--color-primary-light)] hover:bg-[var(--color-primary-lighter)] hover:text-[var(--color-brand-primary)]",
link:
"text-[var(--color-primary)] underline-offset-4 hover:underline hover:text-[var(--color-brand-primary)]",
"text-[var(--color-primary)] underline-offset-4 hover:underline hover:text-[var(--color-brand-primary)] after:absolute after:bottom-0 after:left-0 after:h-[1.5px] after:w-0 hover:after:w-full after:bg-[var(--color-brand-primary)] after:transition-all after:duration-300",
},
size: {
default: "h-11 px-4 py-2",
@@ -45,14 +44,45 @@ export interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, asChild = false, onClick, children, ...props }, ref) => {
const [ripples, setRipples] = React.useState<Array<{ id: number; x: number; y: number }>>([])
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const id = Date.now()
setRipples(prev => [...prev, { id, x, y }])
setTimeout(() => {
setRipples(prev => prev.filter(r => r.id !== id))
}, 600)
onClick?.(e)
}
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
onClick={handleClick}
{...props}
/>
>
{children}
{ripples.map(ripple => (
<span
key={ripple.id}
className="absolute rounded-full bg-white/30 pointer-events-none animate-ripple"
style={{
left: ripple.x,
top: ripple.y,
width: '20px',
height: '20px',
marginLeft: '-10px',
marginTop: '-10px',
}}
/>
))}
</Comp>
)
}
)