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:
2026-03-06 16:21:48 +01:00
parent 2b30ae47de
commit 7cdc72b6e1
30 changed files with 1783 additions and 694 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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";

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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";