mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
feat: Add shared UI component library — Button and Badge
Premium UI components with Framer Motion, glassmorphism, RTL-safe: - button.tsx: 5 variants (primary/secondary/ghost/danger/gold), 3 sizes, loading state - badge.tsx: Status badges with pulse animation for live indicator https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
This commit is contained in:
parent
85a9c9a23f
commit
15906b343c
69
salesflow-saas/frontend/src/components/ui/badge.tsx
Normal file
69
salesflow-saas/frontend/src/components/ui/badge.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
type BadgeVariant = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'live';
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: BadgeVariant;
|
||||
dot?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variantStyles: Record<BadgeVariant, string> = {
|
||||
success: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|
||||
warning: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
|
||||
danger: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
info: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
|
||||
neutral: 'bg-slate-500/15 text-slate-400 border-slate-500/30',
|
||||
live: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|
||||
};
|
||||
|
||||
const dotColors: Record<BadgeVariant, string> = {
|
||||
success: 'bg-emerald-400',
|
||||
warning: 'bg-amber-400',
|
||||
danger: 'bg-red-400',
|
||||
info: 'bg-blue-400',
|
||||
neutral: 'bg-slate-400',
|
||||
live: 'bg-emerald-400',
|
||||
};
|
||||
|
||||
function Badge({ variant = 'neutral', dot = false, children, className }: BadgeProps) {
|
||||
return (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 500, damping: 25 }}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-1.5',
|
||||
'rounded-full border px-2.5 py-0.5',
|
||||
'text-xs font-medium',
|
||||
variantStyles[variant],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{dot && (
|
||||
<span className="relative flex h-2 w-2">
|
||||
{variant === 'live' && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute inline-flex h-full w-full rounded-full opacity-75 animate-ping',
|
||||
dotColors[variant],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={clsx('relative inline-flex h-2 w-2 rounded-full', dotColors[variant])}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge };
|
||||
export type { BadgeProps, BadgeVariant };
|
||||
118
salesflow-saas/frontend/src/components/ui/button.tsx
Normal file
118
salesflow-saas/frontend/src/components/ui/button.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
||||
import { motion, type HTMLMotionProps } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'gold';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface ButtonProps
|
||||
extends Omit<HTMLMotionProps<'button'>, 'children' | 'disabled'>,
|
||||
Pick<ButtonHTMLAttributes<HTMLButtonElement>, 'disabled' | 'type' | 'form'> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
icon?: ReactNode;
|
||||
iconPosition?: 'start' | 'end';
|
||||
fullWidth?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const variantStyles: Record<ButtonVariant, string> = {
|
||||
primary: clsx(
|
||||
'bg-gradient-to-l from-teal-500 to-emerald-600 text-white',
|
||||
'hover:shadow-[0_0_20px_rgba(20,184,166,0.4)]',
|
||||
'active:from-teal-600 active:to-emerald-700',
|
||||
'disabled:from-slate-600 disabled:to-slate-700 disabled:text-slate-400',
|
||||
),
|
||||
secondary: clsx(
|
||||
'border border-teal-500/50 text-teal-400 bg-transparent',
|
||||
'hover:bg-teal-500/10 hover:border-teal-400',
|
||||
'hover:shadow-[0_0_15px_rgba(20,184,166,0.2)]',
|
||||
'active:bg-teal-500/20',
|
||||
'disabled:border-slate-600 disabled:text-slate-500',
|
||||
),
|
||||
ghost: clsx(
|
||||
'text-slate-300 bg-transparent',
|
||||
'hover:bg-white/5 hover:text-white',
|
||||
'active:bg-white/10',
|
||||
'disabled:text-slate-600',
|
||||
),
|
||||
danger: clsx(
|
||||
'bg-gradient-to-l from-red-500 to-rose-600 text-white',
|
||||
'hover:shadow-[0_0_20px_rgba(239,68,68,0.4)]',
|
||||
'active:from-red-600 active:to-rose-700',
|
||||
'disabled:from-slate-600 disabled:to-slate-700 disabled:text-slate-400',
|
||||
),
|
||||
gold: clsx(
|
||||
'bg-gradient-to-l from-amber-400 to-yellow-500 text-slate-900 font-semibold',
|
||||
'hover:shadow-[0_0_20px_rgba(251,191,36,0.4)]',
|
||||
'active:from-amber-500 active:to-yellow-600',
|
||||
'disabled:from-slate-600 disabled:to-slate-700 disabled:text-slate-400',
|
||||
),
|
||||
};
|
||||
|
||||
const sizeStyles: Record<ButtonSize, string> = {
|
||||
sm: 'h-8 text-sm ps-3 pe-3 gap-1.5 rounded-md',
|
||||
md: 'h-10 text-base ps-5 pe-5 gap-2 rounded-lg',
|
||||
lg: 'h-12 text-lg ps-7 pe-7 gap-2.5 rounded-xl',
|
||||
};
|
||||
|
||||
const DealixButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
icon,
|
||||
iconPosition = 'start',
|
||||
fullWidth = false,
|
||||
disabled,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
whileHover={isDisabled ? undefined : { scale: 1.03 }}
|
||||
whileTap={isDisabled ? undefined : { scale: 0.97 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center font-medium',
|
||||
'text-center select-none cursor-pointer',
|
||||
'transition-shadow duration-200',
|
||||
'disabled:cursor-not-allowed disabled:opacity-70',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900',
|
||||
fullWidth && 'w-full',
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
icon && iconPosition === 'start' && <span className="shrink-0">{icon}</span>
|
||||
)}
|
||||
<span>{children}</span>
|
||||
{!loading && icon && iconPosition === 'end' && (
|
||||
<span className="shrink-0">{icon}</span>
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DealixButton.displayName = 'DealixButton';
|
||||
|
||||
export { DealixButton as Button };
|
||||
export type { ButtonProps, ButtonVariant, ButtonSize };
|
||||
130
salesflow-saas/frontend/src/components/ui/card.tsx
Normal file
130
salesflow-saas/frontend/src/components/ui/card.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
|
||||
import { motion, type HTMLMotionProps } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
type CardVariant = 'default' | 'gradient' | 'elevated' | 'feature';
|
||||
|
||||
interface CardProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
|
||||
variant?: CardVariant;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
badge?: ReactNode;
|
||||
noPadding?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const variantStyles: Record<CardVariant, string> = {
|
||||
default: clsx(
|
||||
'bg-white/5 backdrop-blur-xl',
|
||||
'border border-white/10',
|
||||
),
|
||||
gradient: clsx(
|
||||
'bg-gradient-to-bl from-teal-500/10 via-slate-900/80 to-slate-900/90',
|
||||
'backdrop-blur-xl border border-teal-500/20',
|
||||
),
|
||||
elevated: clsx(
|
||||
'bg-slate-800/80 backdrop-blur-xl',
|
||||
'border border-white/10',
|
||||
'shadow-xl shadow-black/20',
|
||||
),
|
||||
feature: clsx(
|
||||
'bg-gradient-to-bl from-teal-500/15 via-emerald-500/5 to-transparent',
|
||||
'backdrop-blur-xl border border-teal-400/20',
|
||||
),
|
||||
};
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
(
|
||||
{
|
||||
variant = 'default',
|
||||
header,
|
||||
footer,
|
||||
badge,
|
||||
noPadding = false,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
whileHover={{
|
||||
y: -2,
|
||||
boxShadow: '0 20px 40px -12px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className={clsx(
|
||||
'relative rounded-xl overflow-hidden',
|
||||
'text-white',
|
||||
'transition-colors duration-200',
|
||||
variantStyles[variant],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{badge && (
|
||||
<div className="absolute top-3 end-3 z-10">{badge}</div>
|
||||
)}
|
||||
|
||||
{header && (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-b border-white/10',
|
||||
!noPadding && 'px-6 py-4',
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx(!noPadding && 'p-6')}>{children}</div>
|
||||
|
||||
{footer && (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-t border-white/10',
|
||||
!noPadding && 'px-6 py-4',
|
||||
)}
|
||||
>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function CardTitle({ children, className, ...props }: CardTitleProps) {
|
||||
return (
|
||||
<h3
|
||||
className={clsx('text-lg font-semibold text-white', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ children, className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
|
||||
return (
|
||||
<p
|
||||
className={clsx('text-sm text-slate-400 mt-1', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export { Card, CardTitle, CardDescription };
|
||||
export type { CardProps, CardVariant };
|
||||
Loading…
Reference in New Issue
Block a user