feat(backoffice): redesign UI with enhanced background and glassmorphism effects
- Add vibrant radial gradient backgrounds with multiple color zones - Implement glassmorphism effects on header and cards - Add subtle grain texture overlay - Update card hover effects with smooth transitions - Improve dark mode background visibility
This commit is contained in:
@@ -1,61 +1,113 @@
|
||||
type BadgeVariant = "default" | "primary" | "success" | "warning" | "error" | "muted";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type BadgeVariant =
|
||||
| "default"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "muted"
|
||||
| "unread"
|
||||
| "in-progress"
|
||||
| "completed";
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
variant?: BadgeVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variantStyles: Record<BadgeVariant, string> = {
|
||||
default: "bg-muted/20 text-muted",
|
||||
primary: "bg-primary-soft text-primary",
|
||||
success: "bg-success-soft text-success",
|
||||
warning: "bg-warning-soft text-warning",
|
||||
error: "bg-error-soft text-error",
|
||||
muted: "bg-muted/10 text-muted",
|
||||
// shadcn/ui compatible
|
||||
default: "bg-primary/90 text-primary-foreground border-transparent hover:bg-primary/80 backdrop-blur-md",
|
||||
secondary: "bg-secondary/80 text-secondary-foreground border-transparent hover:bg-secondary/60 backdrop-blur-md",
|
||||
destructive: "bg-destructive/90 text-destructive-foreground border-transparent hover:bg-destructive/80 backdrop-blur-md",
|
||||
outline: "text-foreground border-border bg-background/50",
|
||||
|
||||
// Legacy + Additional variants
|
||||
primary: "bg-primary/90 text-primary-foreground backdrop-blur-md",
|
||||
success: "bg-success/90 text-success-foreground backdrop-blur-md",
|
||||
warning: "bg-warning/90 text-white backdrop-blur-md",
|
||||
error: "bg-destructive/90 text-destructive-foreground backdrop-blur-md",
|
||||
muted: "bg-muted/60 text-muted-foreground backdrop-blur-md",
|
||||
|
||||
// Status badges from StripStream
|
||||
unread: "badge-unread backdrop-blur-md",
|
||||
"in-progress": "badge-in-progress backdrop-blur-md",
|
||||
completed: "badge-completed backdrop-blur-md",
|
||||
};
|
||||
|
||||
export function Badge({ children, variant = "default", className = "" }: BadgeProps) {
|
||||
return (
|
||||
<span className={`inline-flex px-2.5 py-1 rounded-full text-xs font-semibold ${variantStyles[variant]} ${className}`}>
|
||||
<span className={`
|
||||
inline-flex items-center
|
||||
px-2.5 py-0.5
|
||||
rounded-full
|
||||
text-xs font-semibold
|
||||
border
|
||||
transition-colors duration-200
|
||||
${variantStyles[variant]}
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type StatusVariant = "running" | "success" | "failed" | "cancelled" | "pending";
|
||||
// Status badge for jobs/tasks
|
||||
const statusVariants: Record<string, BadgeVariant> = {
|
||||
running: "in-progress",
|
||||
success: "completed",
|
||||
completed: "completed",
|
||||
failed: "error",
|
||||
cancelled: "muted",
|
||||
pending: "warning",
|
||||
unread: "unread",
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const statusVariants: Record<StatusVariant, BadgeVariant> = {
|
||||
running: "primary",
|
||||
success: "success",
|
||||
failed: "error",
|
||||
cancelled: "muted",
|
||||
pending: "warning",
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, className = "" }: StatusBadgeProps) {
|
||||
const variant = statusVariants[status as StatusVariant] || "default";
|
||||
const variant = statusVariants[status.toLowerCase()] || "default";
|
||||
return <Badge variant={variant} className={className}>{status}</Badge>;
|
||||
}
|
||||
|
||||
type JobTypeVariant = "rebuild" | "full_rebuild";
|
||||
// Job type badge
|
||||
const jobTypeVariants: Record<string, BadgeVariant> = {
|
||||
rebuild: "primary",
|
||||
full_rebuild: "warning",
|
||||
};
|
||||
|
||||
interface JobTypeBadgeProps {
|
||||
type: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const jobTypeVariants: Record<JobTypeVariant, BadgeVariant> = {
|
||||
rebuild: "primary",
|
||||
full_rebuild: "warning",
|
||||
};
|
||||
|
||||
export function JobTypeBadge({ type, className = "" }: JobTypeBadgeProps) {
|
||||
const variant = jobTypeVariants[type as JobTypeVariant] || "default";
|
||||
const variant = jobTypeVariants[type.toLowerCase()] || "default";
|
||||
return <Badge variant={variant} className={className}>{type}</Badge>;
|
||||
}
|
||||
|
||||
// Progress badge (shows percentage)
|
||||
interface ProgressBadgeProps {
|
||||
progress: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ProgressBadge({ progress, className = "" }: ProgressBadgeProps) {
|
||||
let variant: BadgeVariant = "unread";
|
||||
if (progress === 100) variant = "completed";
|
||||
else if (progress > 0) variant = "in-progress";
|
||||
|
||||
return (
|
||||
<Badge variant={variant} className={className}>
|
||||
{progress}%
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
|
||||
type ButtonVariant = "primary" | "secondary" | "danger" | "warning" | "ghost";
|
||||
type ButtonVariant =
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link"
|
||||
| "primary"
|
||||
| "danger"
|
||||
| "warning";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: ReactNode;
|
||||
@@ -9,22 +18,29 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
}
|
||||
|
||||
const variantStyles: Record<ButtonVariant, string> = {
|
||||
primary: "bg-primary text-white hover:bg-primary/90",
|
||||
secondary: "border border-line text-muted hover:bg-muted/5",
|
||||
danger: "bg-error text-white hover:bg-error/90",
|
||||
warning: "bg-warning text-white hover:bg-warning/90",
|
||||
ghost: "text-muted hover:text-foreground hover:bg-muted/5",
|
||||
// shadcn/ui compatible variants
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/85 shadow-sm",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
||||
// Legacy variants (mapped to new ones for compatibility)
|
||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md",
|
||||
danger: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
|
||||
warning: "bg-warning text-white hover:bg-warning/90 shadow-sm",
|
||||
};
|
||||
|
||||
const sizeStyles: Record<string, string> = {
|
||||
sm: "h-8 px-3 text-xs",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
sm: "h-9 px-3 text-xs rounded-md",
|
||||
md: "h-10 px-4 py-2 text-sm rounded-md",
|
||||
lg: "h-11 px-8 text-base rounded-md",
|
||||
};
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant = "primary",
|
||||
variant = "default",
|
||||
size = "md",
|
||||
className = "",
|
||||
disabled,
|
||||
@@ -33,8 +49,12 @@ export function Button({
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
inline-flex items-center justify-center font-medium rounded-lg transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
inline-flex items-center justify-center
|
||||
font-medium
|
||||
transition-all duration-200 ease-out
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:pointer-events-none disabled:opacity-50
|
||||
active:scale-[0.98]
|
||||
${variantStyles[variant]}
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
@@ -46,3 +66,46 @@ export function Button({
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Icon Button variant
|
||||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: ReactNode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: ButtonVariant;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const iconSizeStyles: Record<string, string> = {
|
||||
sm: "h-8 w-8",
|
||||
md: "h-9 w-9",
|
||||
lg: "h-10 w-10",
|
||||
};
|
||||
|
||||
export function IconButton({
|
||||
children,
|
||||
size = "md",
|
||||
variant = "ghost",
|
||||
className = "",
|
||||
title,
|
||||
...props
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
title={title}
|
||||
className={`
|
||||
inline-flex items-center justify-center
|
||||
rounded-md
|
||||
transition-all duration-200 ease-out
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:pointer-events-none disabled:opacity-50
|
||||
hover:bg-accent hover:text-accent-foreground
|
||||
active:scale-[0.96]
|
||||
${iconSizeStyles[size]}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,146 @@ import { ReactNode } from "react";
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
export function Card({ children, className = "" }: CardProps) {
|
||||
export function Card({ children, className = "", hover = true }: CardProps) {
|
||||
return (
|
||||
<div className={`bg-card rounded-xl shadow-soft border border-line p-6 ${className}`}>
|
||||
<div
|
||||
className={`
|
||||
bg-card text-card-foreground
|
||||
rounded-lg border border-border/60
|
||||
shadow-sm
|
||||
transition-all duration-200 ease-out
|
||||
${hover ? "hover:shadow-md hover:-translate-y-0.5" : ""}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardHeaderProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardHeader({ title, className = "" }: CardHeaderProps) {
|
||||
export function CardHeader({ children, className = "" }: CardHeaderProps) {
|
||||
return (
|
||||
<h2 className={`text-lg font-semibold text-foreground mb-4 ${className}`}>
|
||||
{title}
|
||||
</h2>
|
||||
<div className={`flex flex-col space-y-1.5 p-6 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardTitleProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardTitle({ children, className = "" }: CardTitleProps) {
|
||||
return (
|
||||
<h3 className={`text-2xl font-semibold leading-none tracking-tight ${className}`}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardDescriptionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardDescription({ children, className = "" }: CardDescriptionProps) {
|
||||
return (
|
||||
<p className={`text-sm text-muted-foreground ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardContent({ children, className = "" }: CardContentProps) {
|
||||
return (
|
||||
<div className={`p-6 pt-0 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardFooterProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardFooter({ children, className = "" }: CardFooterProps) {
|
||||
return (
|
||||
<div className={`flex items-center p-6 pt-0 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Glass Card variant for special sections
|
||||
interface GlassCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function GlassCard({ children, className = "" }: GlassCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
glass-card
|
||||
rounded-xl
|
||||
p-6
|
||||
transition-all duration-200 ease-out
|
||||
hover:shadow-elevation-2
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple card with header shortcut
|
||||
interface SimpleCardProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export function SimpleCard({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className = "",
|
||||
footer
|
||||
}: SimpleCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
{(title || description) && (
|
||||
<CardHeader>
|
||||
{title && <CardTitle>{title}</CardTitle>}
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent>
|
||||
{children}
|
||||
</CardContent>
|
||||
{footer && (
|
||||
<CardFooter>
|
||||
{footer}
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,81 @@
|
||||
import { ReactNode, LabelHTMLAttributes, InputHTMLAttributes, SelectHTMLAttributes } from "react";
|
||||
|
||||
// Form Field Container
|
||||
interface FormFieldProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormField({ children, className = "" }: FormFieldProps) {
|
||||
return <div className={`flex-1 min-w-48 ${className}`}>{children}</div>;
|
||||
return <div className={`flex flex-col space-y-1.5 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
// Form Label
|
||||
interface FormLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
||||
children: ReactNode;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function FormLabel({ children, className = "", ...props }: FormLabelProps) {
|
||||
export function FormLabel({ children, required, className = "", ...props }: FormLabelProps) {
|
||||
return (
|
||||
<label className={`block text-sm font-medium text-foreground mb-1.5 ${className}`} {...props}>
|
||||
<label
|
||||
className={`text-sm font-medium text-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{required && <span className="text-destructive ml-1">*</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {}
|
||||
// Form Input
|
||||
interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function FormInput({ className = "", ...props }: FormInputProps) {
|
||||
export function FormInput({ className = "", error, ...props }: FormInputProps) {
|
||||
return (
|
||||
<input
|
||||
className={`w-full h-10 px-3 rounded-lg border border-line bg-background text-foreground placeholder-muted focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm ${className}`}
|
||||
className={`
|
||||
flex h-10 w-full
|
||||
rounded-md border border-input
|
||||
bg-background px-3 py-2
|
||||
text-sm
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
file:border-0 file:bg-transparent file:text-sm file:font-medium
|
||||
placeholder:text-muted-foreground/90
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${error ? "border-destructive focus-visible:ring-destructive" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Form Select
|
||||
interface FormSelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
children: ReactNode;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function FormSelect({ children, className = "", ...props }: FormSelectProps) {
|
||||
export function FormSelect({ children, className = "", error, ...props }: FormSelectProps) {
|
||||
return (
|
||||
<select
|
||||
className={`w-full h-10 px-3 rounded-lg border border-line bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm ${className}`}
|
||||
className={`
|
||||
flex h-10 w-full
|
||||
rounded-md border border-input
|
||||
bg-background px-3 py-2
|
||||
text-sm
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${error ? "border-destructive focus-visible:ring-destructive" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -47,11 +83,64 @@ export function FormSelect({ children, className = "", ...props }: FormSelectPro
|
||||
);
|
||||
}
|
||||
|
||||
// Form Row (horizontal layout)
|
||||
interface FormRowProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormRow({ children, className = "" }: FormRowProps) {
|
||||
return <div className={`flex items-end gap-3 flex-wrap ${className}`}>{children}</div>;
|
||||
return <div className={`flex flex-wrap items-end gap-4 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
// Form Section
|
||||
interface FormSectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormSection({ title, description, children, className = "" }: FormSectionProps) {
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{(title || description) && (
|
||||
<div className="space-y-1">
|
||||
{title && <h3 className="text-lg font-medium text-foreground">{title}</h3>}
|
||||
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Form Error Message
|
||||
interface FormErrorProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormError({ children, className = "" }: FormErrorProps) {
|
||||
return (
|
||||
<p className={`text-xs text-destructive ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// Form Description
|
||||
interface FormDescriptionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FormDescription({ children, className = "" }: FormDescriptionProps) {
|
||||
return (
|
||||
<p className={`text-xs text-muted-foreground ${className}`}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,168 @@
|
||||
import { InputHTMLAttributes, SelectHTMLAttributes, ReactNode } from "react";
|
||||
import { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, ReactNode, forwardRef } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
// Input Component
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function Input({ label, className = "", ...props }: InputProps) {
|
||||
return (
|
||||
<input
|
||||
className={`px-4 py-2.5 rounded-lg border border-line bg-background text-foreground placeholder-muted focus:ring-2 focus:ring-primary focus:border-primary ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`
|
||||
flex w-full
|
||||
h-10 px-3 py-2
|
||||
rounded-md border border-input
|
||||
bg-background
|
||||
text-sm text-foreground
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
file:border-0 file:bg-transparent file:text-sm file:font-medium
|
||||
placeholder:text-muted-foreground/90
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${error ? "border-destructive focus-visible:ring-destructive" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-xs text-destructive mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
// Select Component
|
||||
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Select({ label, children, className = "", ...props }: SelectProps) {
|
||||
return (
|
||||
<select
|
||||
className={`px-4 py-2.5 rounded-lg border border-line bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-primary ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ label, error, children, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
ref={ref}
|
||||
className={`
|
||||
flex w-full
|
||||
h-10 px-3 py-2
|
||||
rounded-md border border-input
|
||||
bg-background
|
||||
text-sm text-foreground
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${error ? "border-destructive focus-visible:ring-destructive" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
{error && (
|
||||
<p className="text-xs text-destructive mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Select.displayName = "Select";
|
||||
|
||||
// Textarea Component
|
||||
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={`
|
||||
flex w-full
|
||||
min-h-[80px] px-3 py-2
|
||||
rounded-md border border-input
|
||||
bg-background
|
||||
text-sm text-foreground
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
placeholder:text-muted-foreground/90
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
resize-vertical
|
||||
${error ? "border-destructive focus-visible:ring-destructive" : ""}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-xs text-destructive mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
// Search Input with Icon
|
||||
interface SearchInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
({ icon, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
{icon && (
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`
|
||||
flex w-full
|
||||
h-10 pl-10 pr-4 py-2
|
||||
rounded-md border border-input
|
||||
bg-background
|
||||
text-sm text-foreground
|
||||
shadow-sm
|
||||
transition-colors duration-200
|
||||
placeholder:text-muted-foreground/90
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
SearchInput.displayName = "SearchInput";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Button } from "./Button";
|
||||
import { IconButton } from "./Button";
|
||||
|
||||
interface CursorPaginationProps {
|
||||
hasNextPage: boolean;
|
||||
@@ -44,14 +45,14 @@ export function CursorPagination({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 mt-8 pt-8 border-t border-line">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 mt-8 pt-8 border-t border-border/60">
|
||||
{/* Page size selector */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-muted">Show</span>
|
||||
<span className="text-sm text-muted-foreground">Show</span>
|
||||
<select
|
||||
value={pageSize.toString()}
|
||||
onChange={(e) => changePageSize(Number(e.target.value))}
|
||||
className="w-20 px-3 py-2 text-sm border border-line rounded-lg bg-background text-foreground"
|
||||
className="w-20 px-3 py-2 text-sm rounded-md border border-input bg-background text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors"
|
||||
>
|
||||
{pageSizeOptions.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
@@ -59,33 +60,38 @@ export function CursorPagination({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-muted">per page</span>
|
||||
<span className="text-sm text-muted-foreground">per page</span>
|
||||
</div>
|
||||
|
||||
{/* Count info */}
|
||||
<div className="text-sm text-muted">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {currentCount} items
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center gap-3">
|
||||
{hasPrevPage && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={goToFirst}
|
||||
>
|
||||
← First
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToFirst}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
</svg>
|
||||
First
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToNext}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
Next →
|
||||
Next
|
||||
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,14 +167,14 @@ export function OffsetPagination({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 mt-8 pt-8 border-t border-line">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 mt-8 pt-8 border-t border-border/60">
|
||||
{/* Page size selector */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-muted">Show</span>
|
||||
<span className="text-sm text-muted-foreground">Show</span>
|
||||
<select
|
||||
value={pageSize.toString()}
|
||||
onChange={(e) => changePageSize(Number(e.target.value))}
|
||||
className="w-20 px-3 py-2 text-sm border border-line rounded-lg bg-background text-foreground"
|
||||
className="w-20 px-3 py-2 text-sm rounded-md border border-input bg-background text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors"
|
||||
>
|
||||
{pageSizeOptions.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
@@ -176,34 +182,37 @@ export function OffsetPagination({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-muted">per page</span>
|
||||
<span className="text-sm text-muted-foreground">per page</span>
|
||||
</div>
|
||||
|
||||
{/* Page info */}
|
||||
<div className="text-sm text-muted">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{startItem}-{endItem} of {totalItems}
|
||||
</div>
|
||||
|
||||
{/* Page navigation */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<div className="flex items-center gap-1">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
title="Previous page"
|
||||
>
|
||||
←
|
||||
</Button>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
|
||||
{getPageNumbers().map((page, index) => (
|
||||
<span key={index}>
|
||||
{page === "..." ? (
|
||||
<span className="px-3 py-2 text-sm text-muted">...</span>
|
||||
<span className="px-3 py-2 text-sm text-muted-foreground">...</span>
|
||||
) : (
|
||||
<Button
|
||||
variant={currentPage === page ? "primary" : "ghost"}
|
||||
variant={currentPage === page ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => goToPage(page as number)}
|
||||
className="min-w-[2.5rem]"
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
@@ -211,14 +220,16 @@ export function OffsetPagination({
|
||||
</span>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
title="Next page"
|
||||
>
|
||||
→
|
||||
</Button>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,21 @@ interface ProgressBarProps {
|
||||
max?: number;
|
||||
showLabel?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: "default" | "success" | "warning" | "error";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "h-1.5",
|
||||
md: "h-2",
|
||||
lg: "h-8",
|
||||
lg: "h-4",
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
default: "bg-primary",
|
||||
success: "bg-success",
|
||||
warning: "bg-warning",
|
||||
error: "bg-destructive",
|
||||
};
|
||||
|
||||
export function ProgressBar({
|
||||
@@ -17,18 +25,19 @@ export function ProgressBar({
|
||||
max = 100,
|
||||
showLabel = false,
|
||||
size = "md",
|
||||
variant = "default",
|
||||
className = ""
|
||||
}: ProgressBarProps) {
|
||||
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
return (
|
||||
<div className={`relative ${sizeStyles[size]} bg-line rounded-full overflow-hidden ${className}`}>
|
||||
<div className={`relative ${sizeStyles[size]} bg-muted/50 rounded-full overflow-hidden ${className}`}>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-success rounded-full transition-all duration-300"
|
||||
className={`absolute inset-y-0 left-0 rounded-full transition-all duration-500 ease-out ${variantStyles[variant]}`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
{showLabel && (
|
||||
<span className="absolute inset-0 flex items-center justify-center text-sm font-semibold text-foreground">
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs font-semibold text-foreground">
|
||||
{Math.round(percent)}%
|
||||
</span>
|
||||
)}
|
||||
@@ -36,21 +45,112 @@ export function ProgressBar({
|
||||
);
|
||||
}
|
||||
|
||||
// Mini Progress Bar (for compact displays)
|
||||
interface MiniProgressBarProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
variant?: "default" | "success" | "warning" | "error";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MiniProgressBar({ value, max = 100, className = "" }: MiniProgressBarProps) {
|
||||
export function MiniProgressBar({
|
||||
value,
|
||||
max = 100,
|
||||
variant = "default",
|
||||
className = ""
|
||||
}: MiniProgressBarProps) {
|
||||
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
return (
|
||||
<div className={`flex-1 h-1.5 bg-line rounded-full overflow-hidden ${className}`}>
|
||||
<div className={`flex-1 h-1.5 bg-muted/50 rounded-full overflow-hidden ${className}`}>
|
||||
<div
|
||||
className="h-full bg-success rounded-full transition-all duration-300"
|
||||
className={`h-full rounded-full transition-all duration-500 ease-out ${variantStyles[variant]}`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Progress indicator with status colors based on percentage
|
||||
interface SmartProgressBarProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SmartProgressBar({
|
||||
value,
|
||||
max = 100,
|
||||
size = "md",
|
||||
className = ""
|
||||
}: SmartProgressBarProps) {
|
||||
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
// Determine variant based on percentage
|
||||
let variant: "default" | "success" | "warning" | "error" = "default";
|
||||
if (percent === 100) variant = "success";
|
||||
else if (percent < 25) variant = "error";
|
||||
else if (percent < 50) variant = "warning";
|
||||
|
||||
return <ProgressBar value={value} max={max} size={size} variant={variant} className={className} />;
|
||||
}
|
||||
|
||||
// Circular Progress (for special use cases)
|
||||
interface CircularProgressProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CircularProgress({
|
||||
value,
|
||||
max = 100,
|
||||
size = 40,
|
||||
strokeWidth = 4,
|
||||
className = ""
|
||||
}: CircularProgressProps) {
|
||||
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
const offset = circumference - (percent / 100) * circumference;
|
||||
|
||||
// Determine color based on percentage
|
||||
let color = "hsl(var(--color-primary))";
|
||||
if (percent === 100) color = "hsl(var(--color-success))";
|
||||
else if (percent < 25) color = "hsl(var(--color-destructive))";
|
||||
else if (percent < 50) color = "hsl(var(--color-warning))";
|
||||
|
||||
return (
|
||||
<div className={`relative inline-flex items-center justify-center ${className}`} style={{ width: size, height: size }}>
|
||||
<svg className="transform -rotate-90" width={size} height={size}>
|
||||
<circle
|
||||
className="text-muted-foreground"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
/>
|
||||
<circle
|
||||
stroke={color}
|
||||
fill="transparent"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
className="transition-all duration-500 ease-out"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute text-xs font-semibold text-foreground">
|
||||
{Math.round(percent)}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ interface StatBoxProps {
|
||||
}
|
||||
|
||||
const variantStyles: Record<string, string> = {
|
||||
default: "bg-muted/5",
|
||||
primary: "bg-primary-soft",
|
||||
success: "bg-success-soft",
|
||||
warning: "bg-warning-soft",
|
||||
error: "bg-error-soft",
|
||||
default: "bg-muted/50",
|
||||
primary: "bg-primary/10",
|
||||
success: "bg-success/10",
|
||||
warning: "bg-warning/10",
|
||||
error: "bg-destructive/10",
|
||||
};
|
||||
|
||||
const valueVariantStyles: Record<string, string> = {
|
||||
@@ -20,14 +20,14 @@ const valueVariantStyles: Record<string, string> = {
|
||||
primary: "text-primary",
|
||||
success: "text-success",
|
||||
warning: "text-warning",
|
||||
error: "text-error",
|
||||
error: "text-destructive",
|
||||
};
|
||||
|
||||
export function StatBox({ value, label, variant = "default", className = "" }: StatBoxProps) {
|
||||
return (
|
||||
<div className={`text-center p-4 rounded-lg ${variantStyles[variant]} ${className}`}>
|
||||
<div className={`text-center p-4 rounded-lg transition-colors duration-200 ${variantStyles[variant]} ${className}`}>
|
||||
<span className={`block text-3xl font-bold ${valueVariantStyles[variant]}`}>{value}</span>
|
||||
<span className={`text-xs ${valueVariantStyles[variant]}/80`}>{label}</span>
|
||||
<span className={`text-xs text-muted-foreground`}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
export { Card, CardHeader } from "./Card";
|
||||
export { Badge, StatusBadge, JobTypeBadge } from "./Badge";
|
||||
export {
|
||||
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
|
||||
GlassCard, SimpleCard
|
||||
} from "./Card";
|
||||
export {
|
||||
Badge, StatusBadge, JobTypeBadge, ProgressBadge
|
||||
} from "./Badge";
|
||||
export { StatBox } from "./StatBox";
|
||||
export { ProgressBar, MiniProgressBar } from "./ProgressBar";
|
||||
export { Button } from "./Button";
|
||||
export { Input, Select } from "./Input";
|
||||
export { FormField, FormLabel, FormInput, FormSelect, FormRow } from "./Form";
|
||||
export {
|
||||
ProgressBar, MiniProgressBar, SmartProgressBar, CircularProgress
|
||||
} from "./ProgressBar";
|
||||
export { Button, IconButton } from "./Button";
|
||||
export {
|
||||
Input, Select, Textarea, SearchInput
|
||||
} from "./Input";
|
||||
export {
|
||||
FormField, FormLabel, FormInput, FormSelect, FormRow,
|
||||
FormSection, FormError, FormDescription
|
||||
} from "./Form";
|
||||
export { PageIcon, NavIcon } from "./Icon";
|
||||
export { CursorPagination, OffsetPagination } from "./Pagination";
|
||||
|
||||
Reference in New Issue
Block a user