Generated by Spark: Load more of UI from JSON declarations and break up large components into atomic and create hooks as needed

This commit is contained in:
2026-01-17 12:07:39 +00:00
committed by GitHub
parent 9ca2f9ce44
commit cd4164deb8
10 changed files with 850 additions and 247 deletions

View File

@@ -0,0 +1,34 @@
import { Button, ButtonProps } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface ConfirmButtonProps extends Omit<ButtonProps, 'onClick'> {
onConfirm: () => void | Promise<void>
confirmText?: string
isLoading?: boolean
}
export function ConfirmButton({
onConfirm,
confirmText = 'Are you sure?',
isLoading,
children,
className,
...props
}: ConfirmButtonProps) {
const handleClick = async () => {
if (window.confirm(confirmText)) {
await onConfirm()
}
}
return (
<Button
onClick={handleClick}
disabled={isLoading}
className={cn(className)}
{...props}
>
{isLoading ? 'Loading...' : children}
</Button>
)
}

View File

@@ -0,0 +1,21 @@
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
interface CountBadgeProps {
count: number
max?: number
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
className?: string
}
export function CountBadge({ count, max, variant = 'default', className }: CountBadgeProps) {
const displayValue = max && count > max ? `${max}+` : count.toString()
if (count === 0) return null
return (
<Badge variant={variant} className={cn('ml-2 px-2 py-0.5 text-xs', className)}>
{displayValue}
</Badge>
)
}

View File

@@ -0,0 +1,49 @@
import { Input } from '@/components/ui/input'
import { MagnifyingGlass, X } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
import { useState } from 'react'
interface FilterInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
className?: string
}
export function FilterInput({
value,
onChange,
placeholder = 'Filter...',
className,
}: FilterInputProps) {
const [isFocused, setIsFocused] = useState(false)
return (
<div className={cn('relative', className)}>
<MagnifyingGlass
className={cn(
'absolute left-3 top-1/2 -translate-y-1/2 transition-colors',
isFocused ? 'text-primary' : 'text-muted-foreground'
)}
size={16}
/>
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
className="pl-9 pr-9"
/>
{value && (
<button
onClick={() => onChange('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
type="button"
>
<X size={16} />
</button>
)}
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface MetricCardProps {
label: string
value: string | number
icon?: ReactNode
trend?: {
value: number
direction: 'up' | 'down'
}
className?: string
}
export function MetricCard({ label, value, icon, trend, className }: MetricCardProps) {
return (
<Card className={cn('bg-card/50 backdrop-blur', className)}>
<CardContent className="pt-6">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="text-sm text-muted-foreground mb-1">{label}</div>
<div className="text-3xl font-bold">{value}</div>
{trend && (
<div
className={cn(
'text-sm mt-2',
trend.direction === 'up' ? 'text-green-500' : 'text-red-500'
)}
>
{trend.direction === 'up' ? '↑' : '↓'} {Math.abs(trend.value)}%
</div>
)}
</div>
{icon && <div className="text-muted-foreground">{icon}</div>}
</div>
</CardContent>
</Card>
)
}