mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 14:14:57 +00:00
feat: Delete 38 duplicate atom TSX files with JSON equivalents
Deleted the following JSON-compatible atoms that now route through json-components: - ActionButton, ActionCard, ActionIcon, Alert, AppLogo, Avatar, AvatarGroup - Badge, BindingIndicator, Breadcrumb, Button, ButtonGroup, Calendar, Card - Checkbox, Chip, CircularProgress, Code, CommandPalette, CompletionCard - ComponentPaletteItem, ConfirmButton, ContextMenu, DataSourceBadge, DataTable - Drawer, Form, Heading, HoverCard, PageHeader, SearchInput, Separator - Skeleton, Slider, Spinner, StatusIcon, StepIndicator, Stepper, Switch, Table These atoms are all marked with deleteOldTSX: true and jsonCompatible: true in the registry, and are now fully exported from src/lib/json-ui/json-components. Build status: ✅ PASSING - 38 atom files deleted - 0 TypeScript errors - All 9,408 modules transformed successfully Atoms remaining as TSX files (73): - 8 non-JSON-compatible atoms (e.g., ColorSwatch, Container, DataList) - 65 layout/utility atoms (Flex, Grid, IconButton, Stack, Text, etc.) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,51 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
export interface ActionButtonProps {
|
|
||||||
icon?: ReactNode
|
|
||||||
label: string
|
|
||||||
onClick: () => void
|
|
||||||
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
|
|
||||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
|
||||||
tooltip?: string
|
|
||||||
disabled?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionButton({
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'default',
|
|
||||||
tooltip,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
}: ActionButtonProps) {
|
|
||||||
const button = (
|
|
||||||
<Button
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{icon && <span className="mr-2">{icon}</span>}
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent>{tooltip}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import { CaretRight } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface ActionCardProps {
|
|
||||||
icon?: React.ReactNode
|
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
onClick?: () => void
|
|
||||||
className?: string
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionCard({ icon, title, description, onClick, className, disabled }: ActionCardProps) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
'cursor-pointer transition-all hover:shadow-md hover:border-primary/50',
|
|
||||||
disabled && 'opacity-50 cursor-not-allowed',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
onClick={disabled ? undefined : onClick}
|
|
||||||
>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
{icon && (
|
|
||||||
<div className="flex-shrink-0 p-2 rounded-lg bg-primary/10 text-primary">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-semibold text-sm mb-1">{title}</div>
|
|
||||||
{description && (
|
|
||||||
<div className="text-xs text-muted-foreground line-clamp-2">{description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<CaretRight size={16} className="flex-shrink-0 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Plus, Pencil, Trash, Copy, Download, Upload } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface ActionIconProps {
|
|
||||||
action: 'add' | 'edit' | 'delete' | 'copy' | 'download' | 'upload'
|
|
||||||
size?: number
|
|
||||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionIcon({ action, size = 16, weight = 'regular', className = '' }: ActionIconProps) {
|
|
||||||
const iconMap = {
|
|
||||||
add: Plus,
|
|
||||||
edit: Pencil,
|
|
||||||
delete: Trash,
|
|
||||||
copy: Copy,
|
|
||||||
download: Download,
|
|
||||||
upload: Upload,
|
|
||||||
}
|
|
||||||
|
|
||||||
const IconComponent = iconMap[action]
|
|
||||||
return <IconComponent size={size} weight={weight} className={className} />
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { Info, Warning, CheckCircle, XCircle } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AlertProps {
|
|
||||||
variant?: 'info' | 'warning' | 'success' | 'error'
|
|
||||||
title?: string
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantConfig = {
|
|
||||||
info: {
|
|
||||||
icon: Info,
|
|
||||||
classes: 'bg-blue-50 border-blue-200 text-blue-900',
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
icon: Warning,
|
|
||||||
classes: 'bg-yellow-50 border-yellow-200 text-yellow-900',
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
icon: CheckCircle,
|
|
||||||
classes: 'bg-green-50 border-green-200 text-green-900',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
icon: XCircle,
|
|
||||||
classes: 'bg-red-50 border-red-200 text-red-900',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Alert({ variant = 'info', title, children, className }: AlertProps) {
|
|
||||||
const config = variantConfig[variant]
|
|
||||||
const Icon = config.icon
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex gap-3 p-4 rounded-lg border',
|
|
||||||
config.classes,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<Icon size={20} weight="bold" className="flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
{title && <div className="font-semibold mb-1">{title}</div>}
|
|
||||||
<div className="text-sm">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Code } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
export function AppLogo() {
|
|
||||||
return (
|
|
||||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shrink-0">
|
|
||||||
<Code size={20} weight="duotone" className="text-white sm:w-6 sm:h-6" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AvatarProps {
|
|
||||||
src?: string
|
|
||||||
alt?: string
|
|
||||||
fallback?: string
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
xs: 'w-6 h-6 text-xs',
|
|
||||||
sm: 'w-8 h-8 text-sm',
|
|
||||||
md: 'w-10 h-10 text-base',
|
|
||||||
lg: 'w-12 h-12 text-lg',
|
|
||||||
xl: 'w-16 h-16 text-xl',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Avatar({ src, alt, fallback, size = 'md', className }: AvatarProps) {
|
|
||||||
const initials = fallback || alt?.slice(0, 2).toUpperCase() || '?'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full bg-muted overflow-hidden',
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{src ? (
|
|
||||||
<img src={src} alt={alt} className="w-full h-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<span className="font-medium text-muted-foreground">{initials}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AvatarGroupProps {
|
|
||||||
avatars: {
|
|
||||||
src?: string
|
|
||||||
alt: string
|
|
||||||
fallback: string
|
|
||||||
}[]
|
|
||||||
max?: number
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
xs: 'h-6 w-6 text-xs',
|
|
||||||
sm: 'h-8 w-8 text-xs',
|
|
||||||
md: 'h-10 w-10 text-sm',
|
|
||||||
lg: 'h-12 w-12 text-base',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AvatarGroup({
|
|
||||||
avatars,
|
|
||||||
max = 5,
|
|
||||||
size = 'md',
|
|
||||||
className,
|
|
||||||
}: AvatarGroupProps) {
|
|
||||||
const displayAvatars = avatars.slice(0, max)
|
|
||||||
const remainingCount = Math.max(avatars.length - max, 0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex -space-x-2', className)}>
|
|
||||||
{displayAvatars.map((avatar, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted overflow-hidden',
|
|
||||||
sizeClasses[size]
|
|
||||||
)}
|
|
||||||
title={avatar.alt}
|
|
||||||
>
|
|
||||||
{avatar.src ? (
|
|
||||||
<img src={avatar.src} alt={avatar.alt} className="h-full w-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<span className="font-medium text-foreground">{avatar.fallback}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted',
|
|
||||||
sizeClasses[size]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="font-medium text-foreground">+{remainingCount}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Badge as ShadcnBadge } from '@/components/ui/badge'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
interface BadgeProps {
|
|
||||||
children: ReactNode
|
|
||||||
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
icon?: ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'text-xs px-2 py-0.5',
|
|
||||||
md: 'text-sm px-2.5 py-0.5',
|
|
||||||
lg: 'text-base px-3 py-1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Badge({
|
|
||||||
children,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'md',
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
}: BadgeProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnBadge
|
|
||||||
variant={variant}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1.5',
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{icon && <span className="flex-shrink-0">{icon}</span>}
|
|
||||||
{children}
|
|
||||||
</ShadcnBadge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Link } from '@phosphor-icons/react'
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
interface BindingIndicatorProps {
|
|
||||||
sourceId: string
|
|
||||||
path?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BindingIndicator({ sourceId, path, className = '' }: BindingIndicatorProps) {
|
|
||||||
const bindingText = path ? `${sourceId}.${path}` : sourceId
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-accent/10 text-accent border border-accent/30 ${className}`}>
|
|
||||||
<Link weight="bold" className="w-3 h-3" />
|
|
||||||
<span className="font-mono">{bindingText}</span>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p className="text-xs">Bound to: {bindingText}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { CaretRight } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
|
||||||
label: string
|
|
||||||
href?: string
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BreadcrumbNavProps {
|
|
||||||
items?: BreadcrumbItem[]
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BreadcrumbNav({ items = [], className }: BreadcrumbNavProps) {
|
|
||||||
return (
|
|
||||||
<nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
const isLast = index === items.length - 1
|
|
||||||
const linkClassName = cn(
|
|
||||||
'text-sm transition-colors',
|
|
||||||
isLast ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex items-center gap-2">
|
|
||||||
{item.href ? (
|
|
||||||
<a href={item.href} onClick={item.onClick} className={linkClassName}>
|
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
) : item.onClick ? (
|
|
||||||
<button onClick={item.onClick} className={linkClassName}>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm',
|
|
||||||
isLast ? 'text-foreground font-medium' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!isLast && <CaretRight className="w-4 h-4 text-muted-foreground" />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Breadcrumb = BreadcrumbNav
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Button as ShadcnButton, ButtonProps as ShadcnButtonProps } from '@/components/ui/button'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export interface ButtonProps extends ShadcnButtonProps {
|
|
||||||
children: ReactNode
|
|
||||||
leftIcon?: ReactNode
|
|
||||||
rightIcon?: ReactNode
|
|
||||||
loading?: boolean
|
|
||||||
fullWidth?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({
|
|
||||||
children,
|
|
||||||
leftIcon,
|
|
||||||
rightIcon,
|
|
||||||
loading,
|
|
||||||
fullWidth,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnButton
|
|
||||||
disabled={disabled || loading}
|
|
||||||
className={cn(fullWidth && 'w-full', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
||||||
<span>{children}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
|
|
||||||
<span>{children}</span>
|
|
||||||
{rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ShadcnButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
interface ButtonGroupProps {
|
|
||||||
children: ReactNode
|
|
||||||
orientation?: 'horizontal' | 'vertical'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ButtonGroup({
|
|
||||||
children,
|
|
||||||
orientation = 'horizontal',
|
|
||||||
className,
|
|
||||||
}: ButtonGroupProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'inline-flex',
|
|
||||||
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
|
|
||||||
'[&>button]:rounded-none',
|
|
||||||
'[&>button:first-child]:rounded-l-md',
|
|
||||||
'[&>button:last-child]:rounded-r-md',
|
|
||||||
orientation === 'vertical' && '[&>button:first-child]:rounded-t-md [&>button:first-child]:rounded-l-none',
|
|
||||||
orientation === 'vertical' && '[&>button:last-child]:rounded-b-md [&>button:last-child]:rounded-r-none',
|
|
||||||
'[&>button:not(:last-child)]:border-r-0',
|
|
||||||
orientation === 'vertical' && '[&>button:not(:last-child)]:border-b-0 [&>button:not(:last-child)]:border-r',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Calendar as ShadcnCalendar } from '@/components/ui/calendar'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CalendarProps {
|
|
||||||
selected?: Date
|
|
||||||
onSelect?: (date: Date | undefined) => void
|
|
||||||
mode?: 'single' | 'multiple' | 'range'
|
|
||||||
disabled?: Date | ((date: Date) => boolean)
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Calendar({
|
|
||||||
selected,
|
|
||||||
onSelect,
|
|
||||||
mode = 'single',
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
}: CalendarProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnCalendar
|
|
||||||
mode={mode as any}
|
|
||||||
selected={selected}
|
|
||||||
onSelect={onSelect as any}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn('rounded-md border', className)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CardProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
variant?: 'default' | 'bordered' | 'elevated' | 'flat'
|
|
||||||
padding?: 'none' | 'sm' | 'md' | 'lg'
|
|
||||||
hover?: boolean
|
|
||||||
className?: string
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Card({
|
|
||||||
children,
|
|
||||||
variant = 'default',
|
|
||||||
padding = 'md',
|
|
||||||
hover = false,
|
|
||||||
className,
|
|
||||||
onClick
|
|
||||||
}: CardProps) {
|
|
||||||
const variantStyles = {
|
|
||||||
default: 'bg-card border border-border',
|
|
||||||
bordered: 'bg-background border-2 border-border',
|
|
||||||
elevated: 'bg-card shadow-lg border border-border',
|
|
||||||
flat: 'bg-muted',
|
|
||||||
}
|
|
||||||
|
|
||||||
const paddingStyles = {
|
|
||||||
none: 'p-0',
|
|
||||||
sm: 'p-3',
|
|
||||||
md: 'p-6',
|
|
||||||
lg: 'p-8',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
'rounded-lg transition-all',
|
|
||||||
variantStyles[variant],
|
|
||||||
paddingStyles[padding],
|
|
||||||
hover && 'hover:shadow-md hover:scale-[1.01] cursor-pointer',
|
|
||||||
onClick && 'cursor-pointer',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { Check, Minus } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CheckboxProps {
|
|
||||||
checked: boolean
|
|
||||||
onChange: (checked: boolean) => void
|
|
||||||
label?: string
|
|
||||||
indeterminate?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Checkbox({
|
|
||||||
checked,
|
|
||||||
onChange,
|
|
||||||
label,
|
|
||||||
indeterminate = false,
|
|
||||||
disabled = false,
|
|
||||||
size = 'md',
|
|
||||||
className
|
|
||||||
}: CheckboxProps) {
|
|
||||||
const sizeStyles = {
|
|
||||||
sm: 'w-4 h-4',
|
|
||||||
md: 'w-5 h-5',
|
|
||||||
lg: 'w-6 h-6',
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconSize = {
|
|
||||||
sm: 12,
|
|
||||||
md: 16,
|
|
||||||
lg: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={cn('flex items-center gap-2 cursor-pointer', disabled && 'opacity-50 cursor-not-allowed', className)}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={indeterminate ? 'mixed' : checked}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => !disabled && onChange(!checked)}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center rounded border-2 transition-colors',
|
|
||||||
sizeStyles[size],
|
|
||||||
checked || indeterminate
|
|
||||||
? 'bg-primary border-primary text-primary-foreground'
|
|
||||||
: 'bg-background border-input hover:border-ring'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{indeterminate ? (
|
|
||||||
<Minus size={iconSize[size]} weight="bold" />
|
|
||||||
) : checked ? (
|
|
||||||
<Check size={iconSize[size]} weight="bold" />
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
{label && <span className="text-sm font-medium select-none">{label}</span>}
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { X } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ChipProps {
|
|
||||||
children: ReactNode
|
|
||||||
variant?: 'default' | 'primary' | 'accent' | 'muted'
|
|
||||||
size?: 'sm' | 'md'
|
|
||||||
onRemove?: () => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantClasses = {
|
|
||||||
default: 'bg-secondary text-secondary-foreground',
|
|
||||||
primary: 'bg-primary text-primary-foreground',
|
|
||||||
accent: 'bg-accent text-accent-foreground',
|
|
||||||
muted: 'bg-muted text-muted-foreground',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'px-2 py-0.5 text-xs',
|
|
||||||
md: 'px-3 py-1 text-sm',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Chip({
|
|
||||||
children,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'md',
|
|
||||||
onRemove,
|
|
||||||
className
|
|
||||||
}: ChipProps) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1 rounded-full font-medium',
|
|
||||||
variantClasses[variant],
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{onRemove && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="inline-flex items-center justify-center hover:bg-black/10 rounded-full transition-colors"
|
|
||||||
aria-label="Remove"
|
|
||||||
>
|
|
||||||
<X size={size === 'sm' ? 12 : 14} weight="bold" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Progress } from '@/components/ui/progress'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CircularProgressProps {
|
|
||||||
value: number
|
|
||||||
max?: number
|
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
||||||
showLabel?: boolean
|
|
||||||
strokeWidth?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: { dimension: 48, stroke: 4, fontSize: 'text-xs' },
|
|
||||||
md: { dimension: 64, stroke: 5, fontSize: 'text-sm' },
|
|
||||||
lg: { dimension: 96, stroke: 6, fontSize: 'text-base' },
|
|
||||||
xl: { dimension: 128, stroke: 8, fontSize: 'text-lg' },
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CircularProgress({
|
|
||||||
value,
|
|
||||||
max = 100,
|
|
||||||
size = 'md',
|
|
||||||
showLabel = true,
|
|
||||||
strokeWidth,
|
|
||||||
className,
|
|
||||||
}: CircularProgressProps) {
|
|
||||||
const { dimension, stroke, fontSize } = sizeClasses[size]
|
|
||||||
const actualStroke = strokeWidth || stroke
|
|
||||||
const percentage = Math.min((value / max) * 100, 100)
|
|
||||||
const radius = (dimension - actualStroke) / 2
|
|
||||||
const circumference = radius * 2 * Math.PI
|
|
||||||
const offset = circumference - (percentage / 100) * circumference
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('relative inline-flex items-center justify-center', className)}>
|
|
||||||
<svg width={dimension} height={dimension} className="transform -rotate-90">
|
|
||||||
<circle
|
|
||||||
cx={dimension / 2}
|
|
||||||
cy={dimension / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={actualStroke}
|
|
||||||
fill="none"
|
|
||||||
className="text-muted opacity-20"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx={dimension / 2}
|
|
||||||
cy={dimension / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={actualStroke}
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={circumference}
|
|
||||||
strokeDashoffset={offset}
|
|
||||||
strokeLinecap="round"
|
|
||||||
className="text-primary transition-all duration-300"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{showLabel && (
|
|
||||||
<span className={cn('absolute font-semibold', fontSize)}>
|
|
||||||
{Math.round(percentage)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CodeProps {
|
|
||||||
children: ReactNode
|
|
||||||
inline?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Code({ children, inline = true, className }: CodeProps) {
|
|
||||||
if (inline) {
|
|
||||||
return (
|
|
||||||
<code
|
|
||||||
className={cn(
|
|
||||||
'px-1.5 py-0.5 rounded bg-muted text-foreground font-mono text-sm',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pre
|
|
||||||
className={cn(
|
|
||||||
'p-4 rounded-lg bg-muted text-foreground font-mono text-sm overflow-x-auto',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<code>{children}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from '@/components/ui/command'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
interface CommandOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
icon?: ReactNode
|
|
||||||
onSelect?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommandPaletteProps {
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
placeholder?: string
|
|
||||||
emptyMessage?: string
|
|
||||||
groups: {
|
|
||||||
heading?: string
|
|
||||||
items: CommandOption[]
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommandPalette({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
placeholder = 'Type a command or search...',
|
|
||||||
emptyMessage = 'No results found.',
|
|
||||||
groups,
|
|
||||||
}: CommandPaletteProps) {
|
|
||||||
return (
|
|
||||||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<CommandInput placeholder={placeholder} />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
|
||||||
{groups.map((group, groupIndex) => (
|
|
||||||
<CommandGroup key={groupIndex} heading={group.heading}>
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<CommandItem
|
|
||||||
key={item.value}
|
|
||||||
value={item.value}
|
|
||||||
onSelect={() => {
|
|
||||||
item.onSelect?.()
|
|
||||||
onOpenChange(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
))}
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
|
||||||
import { CheckCircle } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface CompletionCardProps {
|
|
||||||
completionScore: number
|
|
||||||
completionMessage: string
|
|
||||||
isReadyToExport: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CompletionCard({
|
|
||||||
completionScore,
|
|
||||||
completionMessage,
|
|
||||||
isReadyToExport
|
|
||||||
}: CompletionCardProps) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-gradient-to-br from-primary/10 to-accent/10 border-primary/20">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<CheckCircle size={24} weight="duotone" className="text-primary" />
|
|
||||||
Project Completeness
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Overall progress of your application</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-4xl font-bold">{completionScore}%</span>
|
|
||||||
<Badge variant={isReadyToExport ? 'default' : 'secondary'} className="text-sm">
|
|
||||||
{isReadyToExport ? 'Ready to Export' : 'In Progress'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<Progress value={completionScore} className="h-3" />
|
|
||||||
<p className="text-sm text-muted-foreground">{completionMessage}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
|
||||||
import { Card } from '@/components/ui/card'
|
|
||||||
import * as Icons from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ComponentPaletteItemProps {
|
|
||||||
component: ComponentDefinition
|
|
||||||
onDragStart: (component: ComponentDefinition, e: React.DragEvent) => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ComponentPaletteItem({ component, onDragStart, className }: ComponentPaletteItemProps) {
|
|
||||||
const IconComponent = (Icons as any)[component.icon] || Icons.Cube
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => onDragStart(component, e)}
|
|
||||||
className={cn(
|
|
||||||
'p-3 cursor-move hover:bg-accent/50 hover:border-accent transition-all',
|
|
||||||
'flex flex-col items-center gap-2 text-center',
|
|
||||||
'hover:scale-105 active:scale-95',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconComponent className="w-6 h-6 text-primary" weight="duotone" />
|
|
||||||
<span className="text-xs font-medium text-foreground">{component.label}</span>
|
|
||||||
<span className="text-[10px] text-muted-foreground">{component.type}</span>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import {
|
|
||||||
ContextMenu as ShadcnContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
} from '@/components/ui/context-menu'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export interface ContextMenuItemType {
|
|
||||||
label: string
|
|
||||||
icon?: ReactNode
|
|
||||||
shortcut?: string
|
|
||||||
onSelect?: () => void
|
|
||||||
disabled?: boolean
|
|
||||||
separator?: boolean
|
|
||||||
submenu?: ContextMenuItemType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
trigger: ReactNode
|
|
||||||
items: ContextMenuItemType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContextMenu({ trigger, items }: ContextMenuProps) {
|
|
||||||
const renderItems = (menuItems: ContextMenuItemType[]) => {
|
|
||||||
return menuItems.map((item, index) => {
|
|
||||||
if (item.separator) {
|
|
||||||
return <ContextMenuSeparator key={`separator-${index}`} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.submenu && item.submenu.length > 0) {
|
|
||||||
return (
|
|
||||||
<ContextMenuSub key={index}>
|
|
||||||
<ContextMenuSubTrigger>
|
|
||||||
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
||||||
{item.label}
|
|
||||||
</ContextMenuSubTrigger>
|
|
||||||
<ContextMenuSubContent>
|
|
||||||
{renderItems(item.submenu)}
|
|
||||||
</ContextMenuSubContent>
|
|
||||||
</ContextMenuSub>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenuItem
|
|
||||||
key={index}
|
|
||||||
onSelect={item.onSelect}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
||||||
<span className="flex-1">{item.label}</span>
|
|
||||||
{item.shortcut && (
|
|
||||||
<span className="ml-auto text-xs text-muted-foreground">
|
|
||||||
{item.shortcut}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ContextMenuItem>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShadcnContextMenu>
|
|
||||||
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
|
|
||||||
<ContextMenuContent>{renderItems(items)}</ContextMenuContent>
|
|
||||||
</ShadcnContextMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { DataSourceType } from '@/types/json-ui'
|
|
||||||
import { Database, File } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface DataSourceBadgeProps {
|
|
||||||
type: DataSourceType
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataSourceConfig = {
|
|
||||||
kv: {
|
|
||||||
icon: Database,
|
|
||||||
label: 'KV Storage',
|
|
||||||
className: 'bg-accent/20 text-accent border-accent/30'
|
|
||||||
},
|
|
||||||
static: {
|
|
||||||
icon: File,
|
|
||||||
label: 'Static',
|
|
||||||
className: 'bg-muted text-muted-foreground border-border'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataSourceBadge({ type, className = '' }: DataSourceBadgeProps) {
|
|
||||||
const config = dataSourceConfig[type]
|
|
||||||
const Icon = config.icon
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge className={`flex items-center gap-1 ${config.className} ${className}`} variant="outline">
|
|
||||||
<Icon className="w-3 h-3" weight="bold" />
|
|
||||||
<span>{config.label}</span>
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export interface Column<T> {
|
|
||||||
key: string
|
|
||||||
header: string | ReactNode
|
|
||||||
cell?: (item: T) => ReactNode
|
|
||||||
sortable?: boolean
|
|
||||||
width?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataTableProps<T> {
|
|
||||||
data: T[]
|
|
||||||
columns: Column<T>[]
|
|
||||||
onRowClick?: (item: T) => void
|
|
||||||
emptyMessage?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTable<T extends Record<string, any>>({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
onRowClick,
|
|
||||||
emptyMessage = 'No data available',
|
|
||||||
className,
|
|
||||||
}: DataTableProps<T>) {
|
|
||||||
return (
|
|
||||||
<div className={cn('rounded-md border', className)}>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<TableHead
|
|
||||||
key={column.key}
|
|
||||||
style={{ width: column.width }}
|
|
||||||
className={cn(column.sortable && 'cursor-pointer select-none')}
|
|
||||||
>
|
|
||||||
{column.header}
|
|
||||||
</TableHead>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={columns.length} className="text-center py-8 text-muted-foreground">
|
|
||||||
{emptyMessage}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
data.map((item, rowIndex) => (
|
|
||||||
<TableRow
|
|
||||||
key={rowIndex}
|
|
||||||
onClick={() => onRowClick?.(item)}
|
|
||||||
className={cn(onRowClick && 'cursor-pointer')}
|
|
||||||
>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<TableCell key={column.key}>
|
|
||||||
{column.cell ? column.cell(item) : item[column.key]}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { X } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface DrawerProps {
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
title?: string
|
|
||||||
children: React.ReactNode
|
|
||||||
position?: 'left' | 'right' | 'top' | 'bottom'
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
showCloseButton?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Drawer({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
position = 'right',
|
|
||||||
size = 'md',
|
|
||||||
showCloseButton = true,
|
|
||||||
className,
|
|
||||||
}: DrawerProps) {
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const positionStyles = {
|
|
||||||
left: 'left-0 top-0 h-full',
|
|
||||||
right: 'right-0 top-0 h-full',
|
|
||||||
top: 'top-0 left-0 w-full',
|
|
||||||
bottom: 'bottom-0 left-0 w-full',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeStyles = {
|
|
||||||
sm: position === 'left' || position === 'right' ? 'w-64' : 'h-64',
|
|
||||||
md: position === 'left' || position === 'right' ? 'w-96' : 'h-96',
|
|
||||||
lg: position === 'left' || position === 'right' ? 'w-[600px]' : 'h-[600px]',
|
|
||||||
}
|
|
||||||
|
|
||||||
const slideAnimation = {
|
|
||||||
left: 'animate-in slide-in-from-left',
|
|
||||||
right: 'animate-in slide-in-from-right',
|
|
||||||
top: 'animate-in slide-in-from-top',
|
|
||||||
bottom: 'animate-in slide-in-from-bottom',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-in fade-in-0"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'fixed z-50 bg-card border border-border shadow-lg',
|
|
||||||
positionStyles[position],
|
|
||||||
sizeStyles[size],
|
|
||||||
slideAnimation[position],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(title || showCloseButton) && (
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
|
||||||
{title && <h2 className="text-lg font-semibold">{title}</h2>}
|
|
||||||
{showCloseButton && (
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="ml-auto p-1 rounded-md hover:bg-accent transition-colors"
|
|
||||||
aria-label="Close drawer"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="p-6 overflow-auto h-full">{children}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import {
|
|
||||||
Form as ShadcnForm,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@/components/ui/form'
|
|
||||||
import { UseFormReturn } from 'react-hook-form'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
interface FormProps {
|
|
||||||
form: UseFormReturn<any>
|
|
||||||
onSubmit: (values: any) => void | Promise<void>
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Form({ form, onSubmit, children, className }: FormProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnForm {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className={className}>
|
|
||||||
{children}
|
|
||||||
</form>
|
|
||||||
</ShadcnForm>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage }
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { ReactNode, createElement } from 'react'
|
|
||||||
|
|
||||||
interface HeadingProps {
|
|
||||||
children: ReactNode
|
|
||||||
level?: 1 | 2 | 3 | 4 | 5 | 6
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelClasses = {
|
|
||||||
1: 'text-4xl font-bold tracking-tight',
|
|
||||||
2: 'text-3xl font-semibold tracking-tight',
|
|
||||||
3: 'text-2xl font-semibold tracking-tight',
|
|
||||||
4: 'text-xl font-semibold',
|
|
||||||
5: 'text-lg font-medium',
|
|
||||||
6: 'text-base font-medium',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Heading({ children, level = 1, className = '' }: HeadingProps) {
|
|
||||||
return createElement(
|
|
||||||
`h${level}`,
|
|
||||||
{ className: `${levelClasses[level]} ${className}` },
|
|
||||||
children
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import {
|
|
||||||
HoverCard as ShadcnHoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from '@/components/ui/hover-card'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface HoverCardProps {
|
|
||||||
trigger: ReactNode
|
|
||||||
children: ReactNode
|
|
||||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
||||||
align?: 'start' | 'center' | 'end'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HoverCard({
|
|
||||||
trigger,
|
|
||||||
children,
|
|
||||||
side = 'bottom',
|
|
||||||
align = 'center',
|
|
||||||
className,
|
|
||||||
}: HoverCardProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnHoverCard>
|
|
||||||
<HoverCardTrigger asChild>{trigger}</HoverCardTrigger>
|
|
||||||
<HoverCardContent side={side} align={align} className={cn(className)}>
|
|
||||||
{children}
|
|
||||||
</HoverCardContent>
|
|
||||||
</ShadcnHoverCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Separator as ShadcnSeparator } from '@/components/ui/separator'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SeparatorProps {
|
|
||||||
orientation?: 'horizontal' | 'vertical'
|
|
||||||
decorative?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Separator({
|
|
||||||
orientation = 'horizontal',
|
|
||||||
decorative = true,
|
|
||||||
className,
|
|
||||||
}: SeparatorProps) {
|
|
||||||
return (
|
|
||||||
<ShadcnSeparator
|
|
||||||
orientation={orientation}
|
|
||||||
decorative={decorative}
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SkeletonProps {
|
|
||||||
variant?: 'text' | 'rectangular' | 'circular' | 'rounded'
|
|
||||||
width?: string | number
|
|
||||||
height?: string | number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantClasses = {
|
|
||||||
text: 'rounded h-4',
|
|
||||||
rectangular: 'rounded-none',
|
|
||||||
circular: 'rounded-full',
|
|
||||||
rounded: 'rounded-lg',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Skeleton({
|
|
||||||
variant = 'rectangular',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
className
|
|
||||||
}: SkeletonProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'bg-muted animate-pulse',
|
|
||||||
variantClasses[variant],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width: typeof width === 'number' ? `${width}px` : width,
|
|
||||||
height: typeof height === 'number' ? `${height}px` : height,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SliderProps {
|
|
||||||
value: number
|
|
||||||
onChange: (value: number) => void
|
|
||||||
min?: number
|
|
||||||
max?: number
|
|
||||||
step?: number
|
|
||||||
label?: string
|
|
||||||
showValue?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Slider({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
min = 0,
|
|
||||||
max = 100,
|
|
||||||
step = 1,
|
|
||||||
label,
|
|
||||||
showValue = false,
|
|
||||||
disabled = false,
|
|
||||||
className
|
|
||||||
}: SliderProps) {
|
|
||||||
const percentage = ((value - min) / (max - min)) * 100
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full', className)}>
|
|
||||||
{(label || showValue) && (
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
{label && <span className="text-sm font-medium">{label}</span>}
|
|
||||||
{showValue && <span className="text-sm text-muted-foreground">{value}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
|
||||||
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',
|
|
||||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
||||||
disabled && 'opacity-50 cursor-not-allowed',
|
|
||||||
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5',
|
|
||||||
'[&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:rounded-full',
|
|
||||||
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-transform',
|
|
||||||
'[&::-webkit-slider-thumb]:hover:scale-110',
|
|
||||||
'[&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:bg-primary',
|
|
||||||
'[&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:rounded-full',
|
|
||||||
'[&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:transition-transform',
|
|
||||||
'[&::-moz-range-thumb]:hover:scale-110'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(to right, hsl(var(--primary)) 0%, hsl(var(--primary)) ${percentage}%, hsl(var(--secondary)) ${percentage}%, hsl(var(--secondary)) 100%)`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { CircleNotch } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SpinnerProps {
|
|
||||||
size?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Spinner({ size = 24, className }: SpinnerProps) {
|
|
||||||
return (
|
|
||||||
<CircleNotch
|
|
||||||
size={size}
|
|
||||||
weight="bold"
|
|
||||||
className={cn('animate-spin text-primary', className)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { CheckCircle, CloudCheck } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface StatusIconProps {
|
|
||||||
type: 'saved' | 'synced'
|
|
||||||
size?: number
|
|
||||||
animate?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusIcon({ type, size = 14, animate = false }: StatusIconProps) {
|
|
||||||
const baseClassName = type === 'saved' ? 'text-accent' : ''
|
|
||||||
const animateClassName = animate ? 'animate-in zoom-in duration-200' : ''
|
|
||||||
const className = [baseClassName, animateClassName].filter(Boolean).join(' ')
|
|
||||||
|
|
||||||
if (type === 'saved') {
|
|
||||||
return (
|
|
||||||
<CheckCircle
|
|
||||||
size={size}
|
|
||||||
weight="fill"
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CloudCheck size={size} weight="duotone" />
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Check } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface StepIndicatorProps {
|
|
||||||
steps: Array<{
|
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
}>
|
|
||||||
currentStep: string
|
|
||||||
completedSteps?: string[]
|
|
||||||
onStepClick?: (stepId: string) => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StepIndicator({
|
|
||||||
steps,
|
|
||||||
currentStep,
|
|
||||||
completedSteps = [],
|
|
||||||
onStepClick,
|
|
||||||
className
|
|
||||||
}: StepIndicatorProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn('flex items-center gap-2', className)}>
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const isCompleted = completedSteps.includes(step.id)
|
|
||||||
const isCurrent = step.id === currentStep
|
|
||||||
const isClickable = !!onStepClick
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={step.id} className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2',
|
|
||||||
isClickable && 'cursor-pointer'
|
|
||||||
)}
|
|
||||||
onClick={() => isClickable && onStepClick(step.id)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium transition-colors',
|
|
||||||
isCompleted && 'bg-accent text-accent-foreground',
|
|
||||||
isCurrent && !isCompleted && 'bg-primary text-primary-foreground',
|
|
||||||
!isCurrent && !isCompleted && 'bg-muted text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCompleted ? <Check size={16} weight="bold" /> : index + 1}
|
|
||||||
</div>
|
|
||||||
<span className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
isCurrent && 'text-foreground',
|
|
||||||
!isCurrent && 'text-muted-foreground'
|
|
||||||
)}>
|
|
||||||
{step.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={cn(
|
|
||||||
'w-8 h-0.5',
|
|
||||||
completedSteps.includes(steps[index + 1].id) ? 'bg-accent' : 'bg-border'
|
|
||||||
)} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Check } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface Step {
|
|
||||||
label: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StepperProps {
|
|
||||||
steps: Step[]
|
|
||||||
currentStep: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Stepper({ steps, currentStep, className }: StepperProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full', className)}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const isCompleted = index < currentStep
|
|
||||||
const isCurrent = index === currentStep
|
|
||||||
const isLast = index === steps.length - 1
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex items-center flex-1">
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-colors',
|
|
||||||
isCompleted && 'bg-primary text-primary-foreground',
|
|
||||||
isCurrent && 'bg-primary text-primary-foreground ring-4 ring-primary/20',
|
|
||||||
!isCompleted && !isCurrent && 'bg-muted text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCompleted ? <Check weight="bold" /> : index + 1}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
(isCompleted || isCurrent) ? 'text-foreground' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{step.label}
|
|
||||||
</div>
|
|
||||||
{step.description && (
|
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">
|
|
||||||
{step.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isLast && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex-1 h-0.5 mx-4 transition-colors',
|
|
||||||
isCompleted ? 'bg-primary' : 'bg-muted'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Switch as ShadcnSwitch } from '@/components/ui/switch'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SwitchProps {
|
|
||||||
checked: boolean
|
|
||||||
onCheckedChange: (checked: boolean) => void
|
|
||||||
label?: string
|
|
||||||
description?: string
|
|
||||||
disabled?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Switch({
|
|
||||||
checked,
|
|
||||||
onCheckedChange,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
}: SwitchProps) {
|
|
||||||
if (!label) {
|
|
||||||
return (
|
|
||||||
<ShadcnSwitch
|
|
||||||
checked={checked}
|
|
||||||
onCheckedChange={onCheckedChange}
|
|
||||||
disabled={disabled}
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex items-center justify-between gap-4', className)}>
|
|
||||||
<div className="flex-1 space-y-1">
|
|
||||||
<Label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
{description && (
|
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ShadcnSwitch
|
|
||||||
checked={checked}
|
|
||||||
onCheckedChange={onCheckedChange}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface TableColumn<T> {
|
|
||||||
key: keyof T | string
|
|
||||||
header: string
|
|
||||||
render?: (item: T) => React.ReactNode
|
|
||||||
width?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableProps<T> {
|
|
||||||
data: T[]
|
|
||||||
columns: TableColumn<T>[]
|
|
||||||
onRowClick?: (item: T) => void
|
|
||||||
striped?: boolean
|
|
||||||
hoverable?: boolean
|
|
||||||
compact?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Table<T extends Record<string, any>>({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
onRowClick,
|
|
||||||
striped = false,
|
|
||||||
hoverable = true,
|
|
||||||
compact = false,
|
|
||||||
className,
|
|
||||||
}: TableProps<T>) {
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full overflow-auto', className)}>
|
|
||||||
<table className="w-full border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-border bg-muted/50">
|
|
||||||
{columns.map((column, index) => (
|
|
||||||
<th
|
|
||||||
key={index}
|
|
||||||
className={cn(
|
|
||||||
'text-left font-medium text-sm text-muted-foreground',
|
|
||||||
compact ? 'px-3 py-2' : 'px-4 py-3',
|
|
||||||
column.width && `w-[${column.width}]`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{column.header}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data.map((item, rowIndex) => (
|
|
||||||
<tr
|
|
||||||
key={rowIndex}
|
|
||||||
onClick={() => onRowClick?.(item)}
|
|
||||||
className={cn(
|
|
||||||
'border-b border-border transition-colors',
|
|
||||||
striped && rowIndex % 2 === 1 && 'bg-muted/30',
|
|
||||||
hoverable && 'hover:bg-muted/50',
|
|
||||||
onRowClick && 'cursor-pointer'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{columns.map((column, colIndex) => (
|
|
||||||
<td
|
|
||||||
key={colIndex}
|
|
||||||
className={cn(
|
|
||||||
'text-sm',
|
|
||||||
compact ? 'px-3 py-2' : 'px-4 py-3'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{column.render
|
|
||||||
? column.render(item)
|
|
||||||
: item[column.key as keyof T]}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{data.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
No data available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||||
import { getCategoryComponents } from '@/lib/component-definition-utils'
|
import { getCategoryComponents } from '@/lib/component-definition-utils'
|
||||||
import { ComponentPaletteItem } from '@/components/atoms/ComponentPaletteItem'
|
import { ComponentPaletteItem } from '@/components/atoms'
|
||||||
import { PanelHeader, Stack } from '@/components/atoms'
|
import { PanelHeader, Stack } from '@/components/atoms'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
import { DataSourceBadge } from '@/components/atoms'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { Pencil, Trash } from '@phosphor-icons/react'
|
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { DataSource } from '@/types/json-ui'
|
import { DataSource } from '@/types/json-ui'
|
||||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
import { DataSourceBadge } from '@/components/atoms'
|
||||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||||
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { UIComponent } from '@/types/json-ui'
|
import { UIComponent } from '@/types/json-ui'
|
||||||
import { ComponentTreeNode } from '@/components/atoms/ComponentTreeNode'
|
import { ComponentTreeNode } from '@/components/atoms'
|
||||||
|
|
||||||
interface ComponentTreeNodesProps {
|
interface ComponentTreeNodesProps {
|
||||||
components: UIComponent[]
|
components: UIComponent[]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PropertyEditorField } from '@/components/atoms/PropertyEditorField'
|
import { PropertyEditorField } from '@/components/atoms'
|
||||||
import { Stack, Text } from '@/components/atoms'
|
import { Stack, Text } from '@/components/atoms'
|
||||||
import { PropertyEditorFieldDefinition } from '@/components/molecules/property-editor/propertyEditorConfig'
|
import { PropertyEditorFieldDefinition } from '@/components/molecules/property-editor/propertyEditorConfig'
|
||||||
import { UIComponent } from '@/types/json-ui'
|
import { UIComponent } from '@/types/json-ui'
|
||||||
|
|||||||
Reference in New Issue
Block a user