feat: add Timestamp and Toggle components; implement TreeIcon and EditorActions components

- Introduced Timestamp component for displaying formatted dates and relative time.
- Added Toggle component for switch-like functionality with customizable sizes.
- Implemented TreeIcon component for rendering tree icons using Phosphor icons.
- Created EditorActions component for explain and improve actions with icons.
- Developed FileTabs component for managing open files with close functionality.
- Added LazyInlineMonacoEditor and LazyMonacoEditor for lazy loading Monaco editor.
- Implemented NavigationItem for navigation with badges and icons.
- Created PageHeaderContent for displaying page headers with icons and descriptions.
- Added JSON configuration files for various UI components and layouts.
- Enhanced data binding with new computed data source hook.
- Updated component registry and types for new components.
- Configured Vite for improved hot module replacement experience.
This commit is contained in:
2026-01-18 21:42:51 +00:00
parent f69220e7e4
commit bef28e8c91
97 changed files with 4422 additions and 56 deletions

View File

@@ -0,0 +1,22 @@
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} />
}

View File

@@ -0,0 +1,51 @@
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>
)
}

View File

@@ -0,0 +1,9 @@
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>
)
}

View File

@@ -0,0 +1,37 @@
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>
)
}

View File

@@ -0,0 +1,60 @@
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>
)
}

View File

@@ -0,0 +1,53 @@
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

View File

@@ -0,0 +1,33 @@
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>
)
}

View File

@@ -0,0 +1,60 @@
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>
)
}

View File

@@ -0,0 +1,54 @@
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>
)
}

View File

@@ -0,0 +1,34 @@
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>
)
}

View File

@@ -0,0 +1,46 @@
import { Check } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface ColorSwatchProps {
color: string
selected?: boolean
onClick?: () => void
size?: 'sm' | 'md' | 'lg'
label?: string
className?: string
}
export function ColorSwatch({
color,
selected = false,
onClick,
size = 'md',
label,
className
}: ColorSwatchProps) {
const sizeStyles = {
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-10 h-10',
}
return (
<div className={cn('flex flex-col items-center gap-1', className)}>
<button
type="button"
onClick={onClick}
className={cn(
'rounded border-2 transition-all flex items-center justify-center',
sizeStyles[size],
selected ? 'border-primary ring-2 ring-ring ring-offset-2' : 'border-border hover:border-ring',
onClick && 'cursor-pointer'
)}
style={{ backgroundColor: color }}
aria-label={label || `Color ${color}`}
>
{selected && <Check className="text-white drop-shadow-lg" weight="bold" />}
</button>
{label && <span className="text-xs text-muted-foreground">{label}</span>}
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface ContainerProps {
children: ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
className?: string
}
const sizeClasses = {
sm: 'max-w-screen-sm',
md: 'max-w-screen-md',
lg: 'max-w-screen-lg',
xl: 'max-w-screen-xl',
full: 'max-w-full',
}
export function Container({ children, size = 'xl', className }: ContainerProps) {
return (
<div className={cn('mx-auto px-4 sm:px-6 lg:px-8', sizeClasses[size], className)}>
{children}
</div>
)
}

View File

@@ -0,0 +1,55 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
export interface DataListProps {
items: any[]
renderItem?: (item: any, index: number) => ReactNode
emptyMessage?: string
className?: string
itemClassName?: string
itemKey?: string
}
export function DataList({
items,
renderItem,
emptyMessage = 'No items',
className,
itemClassName,
itemKey,
}: DataListProps) {
if (items.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
{emptyMessage}
</div>
)
}
const renderFallbackItem = (item: any) => {
if (itemKey && item && typeof item === 'object') {
const value = item[itemKey]
if (value !== undefined && value !== null) {
return typeof value === 'string' || typeof value === 'number'
? value
: JSON.stringify(value)
}
}
if (typeof item === 'string' || typeof item === 'number') {
return item
}
return JSON.stringify(item)
}
return (
<div className={cn('space-y-2', className)}>
{items.map((item, index) => (
<div key={index} className={cn('transition-colors', itemClassName)}>
{renderItem ? renderItem(item, index) : renderFallbackItem(item)}
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { cn } from '@/lib/utils'
interface DividerProps {
orientation?: 'horizontal' | 'vertical'
className?: string
decorative?: boolean
}
export function Divider({
orientation = 'horizontal',
className,
decorative = true
}: DividerProps) {
return (
<div
role={decorative ? 'presentation' : 'separator'}
aria-orientation={orientation}
className={cn(
'bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'w-[1px] h-full',
className
)}
/>
)
}

View File

@@ -0,0 +1,53 @@
import { cn } from '@/lib/utils'
interface DotProps {
variant?: 'default' | 'primary' | 'accent' | 'success' | 'warning' | 'error'
size?: 'xs' | 'sm' | 'md' | 'lg'
pulse?: boolean
className?: string
}
const variantClasses = {
default: 'bg-muted-foreground',
primary: 'bg-primary',
accent: 'bg-accent',
success: 'bg-green-500',
warning: 'bg-yellow-500',
error: 'bg-destructive',
}
const sizeClasses = {
xs: 'w-1.5 h-1.5',
sm: 'w-2 h-2',
md: 'w-3 h-3',
lg: 'w-4 h-4',
}
export function Dot({
variant = 'default',
size = 'sm',
pulse = false,
className
}: DotProps) {
return (
<span className="relative inline-flex">
<span
className={cn(
'inline-block rounded-full',
variantClasses[variant],
sizeClasses[size],
className
)}
/>
{pulse && (
<span
className={cn(
'absolute inline-flex rounded-full opacity-75 animate-ping',
variantClasses[variant],
sizeClasses[size]
)}
/>
)}
</span>
)
}

View File

@@ -0,0 +1,80 @@
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>
</>
)
}

View File

@@ -0,0 +1,17 @@
interface EmptyStateIconProps {
icon: React.ReactNode
variant?: 'default' | 'muted'
}
export function EmptyStateIcon({ icon, variant = 'muted' }: EmptyStateIconProps) {
const variantClasses = {
default: 'from-primary/20 to-accent/20 text-primary',
muted: 'from-muted to-muted/50 text-muted-foreground',
}
return (
<div className={`w-16 h-16 rounded-full bg-gradient-to-br ${variantClasses[variant]} flex items-center justify-center`}>
{icon}
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { FileCode, FileJs, FilePlus } from '@phosphor-icons/react'
interface FileIconProps {
type?: 'code' | 'json' | 'plus'
size?: number
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
className?: string
}
export function FileIcon({ type = 'code', size = 20, weight = 'regular', className = '' }: FileIconProps) {
const iconMap = {
code: FileCode,
json: FileJs,
plus: FilePlus,
}
const IconComponent = iconMap[type]
return <IconComponent size={size} weight={weight} className={className} />
}

View File

@@ -0,0 +1,83 @@
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface FlexProps {
children: ReactNode
direction?: 'row' | 'col' | 'row-reverse' | 'col-reverse'
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
wrap?: 'wrap' | 'nowrap' | 'wrap-reverse'
grow?: boolean
shrink?: boolean
className?: string
}
const directionClasses = {
row: 'flex-row',
col: 'flex-col',
'row-reverse': 'flex-row-reverse',
'col-reverse': 'flex-col-reverse',
}
const alignClasses = {
start: 'items-start',
center: 'items-center',
end: 'items-end',
stretch: 'items-stretch',
baseline: 'items-baseline',
}
const justifyClasses = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
around: 'justify-around',
evenly: 'justify-evenly',
}
const gapClasses = {
none: 'gap-0',
xs: 'gap-1',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
}
const wrapClasses = {
wrap: 'flex-wrap',
nowrap: 'flex-nowrap',
'wrap-reverse': 'flex-wrap-reverse',
}
export function Flex({
children,
direction = 'row',
align = 'stretch',
justify = 'start',
gap = 'md',
wrap = 'nowrap',
grow = false,
shrink = false,
className,
}: FlexProps) {
return (
<div
className={cn(
'flex',
directionClasses[direction],
alignClasses[align],
justifyClasses[justify],
gapClasses[gap],
wrapClasses[wrap],
grow && 'flex-grow',
shrink && 'flex-shrink',
className
)}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { ReactNode } from 'react'
interface GridProps {
children: ReactNode
cols?: 1 | 2 | 3 | 4 | 6 | 12
gap?: 1 | 2 | 3 | 4 | 6 | 8
className?: string
}
const colsClasses = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
12: 'grid-cols-3 md:grid-cols-6 lg:grid-cols-12',
}
const gapClasses = {
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
6: 'gap-6',
8: 'gap-8',
}
export function Grid({ children, cols = 1, gap = 4, className = '' }: GridProps) {
return (
<div className={`grid ${colsClasses[cols]} ${gapClasses[gap]} ${className}`}>
{children}
</div>
)
}

View File

@@ -0,0 +1,24 @@
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
)
}

View File

@@ -0,0 +1,22 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface HelperTextProps {
children: ReactNode
variant?: 'default' | 'error' | 'success'
className?: string
}
const variantClasses = {
default: 'text-muted-foreground',
error: 'text-destructive',
success: 'text-green-600',
}
export function HelperText({ children, variant = 'default', className }: HelperTextProps) {
return (
<p className={cn('text-xs mt-1', variantClasses[variant], className)}>
{children}
</p>
)
}

View File

@@ -0,0 +1,36 @@
import { cn } from '@/lib/utils'
interface IconTextProps {
icon: React.ReactNode
children: React.ReactNode
gap?: 'sm' | 'md' | 'lg'
align?: 'start' | 'center' | 'end'
className?: string
}
export function IconText({
icon,
children,
gap = 'md',
align = 'center',
className
}: IconTextProps) {
const gapStyles = {
sm: 'gap-1',
md: 'gap-2',
lg: 'gap-3',
}
const alignStyles = {
start: 'items-start',
center: 'items-center',
end: 'items-end',
}
return (
<div className={cn('flex', gapStyles[gap], alignStyles[align], className)}>
<span className="flex-shrink-0">{icon}</span>
<span className="flex-1">{children}</span>
</div>
)
}

View File

@@ -0,0 +1,32 @@
interface IconWrapperProps {
icon: React.ReactNode
size?: 'sm' | 'md' | 'lg'
variant?: 'default' | 'muted' | 'primary' | 'destructive'
className?: string
}
export function IconWrapper({
icon,
size = 'md',
variant = 'default',
className = ''
}: IconWrapperProps) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
}
const variantClasses = {
default: 'text-foreground',
muted: 'text-muted-foreground',
primary: 'text-primary',
destructive: 'text-destructive',
}
return (
<span className={`inline-flex items-center justify-center ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}>
{icon}
</span>
)
}

View File

@@ -0,0 +1,41 @@
import { cn } from '@/lib/utils'
import { Info, Warning, CheckCircle, XCircle } from '@phosphor-icons/react'
interface InfoBoxProps {
type?: 'info' | 'warning' | 'success' | 'error'
title?: string
children: React.ReactNode
className?: string
}
const iconMap = {
info: Info,
warning: Warning,
success: CheckCircle,
error: XCircle,
}
const variantClasses = {
info: 'bg-blue-500/10 border-blue-500/20 text-blue-700 dark:text-blue-300',
warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-700 dark:text-yellow-300',
success: 'bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-300',
error: 'bg-destructive/10 border-destructive/20 text-destructive',
}
export function InfoBox({ type = 'info', title, children, className }: InfoBoxProps) {
const Icon = iconMap[type]
return (
<div className={cn(
'flex gap-3 p-4 rounded-lg border',
variantClasses[type],
className
)}>
<Icon size={20} weight="fill" className="flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
{title && <div className="font-semibold mb-1">{title}</div>}
<div className="text-sm opacity-90">{children}</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface InfoPanelProps {
children: ReactNode
variant?: 'info' | 'warning' | 'success' | 'error' | 'default'
title?: string
icon?: ReactNode
className?: string
}
const variantClasses = {
default: 'bg-card border-border',
info: 'bg-blue-500/10 border-blue-500/20 text-blue-700 dark:text-blue-300',
warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-700 dark:text-yellow-300',
success: 'bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-300',
error: 'bg-red-500/10 border-red-500/20 text-red-700 dark:text-red-300',
}
export function InfoPanel({
children,
variant = 'default',
title,
icon,
className,
}: InfoPanelProps) {
return (
<div
className={cn(
'rounded-lg border p-4',
variantClasses[variant],
className
)}
>
{(title || icon) && (
<div className="flex items-center gap-2 mb-2">
{icon && <div className="flex-shrink-0">{icon}</div>}
{title && <div className="font-semibold">{title}</div>}
</div>
)}
<div className="text-sm">{children}</div>
</div>
)
}

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface KbdProps {
children: ReactNode
className?: string
}
export function Kbd({ children, className }: KbdProps) {
return (
<kbd
className={cn(
'inline-flex items-center justify-center px-2 py-1 text-xs font-mono font-semibold',
'bg-muted text-foreground border border-border rounded shadow-sm',
className
)}
>
{children}
</kbd>
)
}

View File

@@ -0,0 +1,34 @@
import { cn } from '@/lib/utils'
interface KeyValueProps {
label: string
value: React.ReactNode
orientation?: 'horizontal' | 'vertical'
className?: string
labelClassName?: string
valueClassName?: string
}
export function KeyValue({
label,
value,
orientation = 'horizontal',
className,
labelClassName,
valueClassName
}: KeyValueProps) {
return (
<div className={cn(
'flex gap-2',
orientation === 'vertical' ? 'flex-col' : 'flex-row items-center justify-between',
className
)}>
<span className={cn('text-sm text-muted-foreground', labelClassName)}>
{label}
</span>
<span className={cn('text-sm font-medium', valueClassName)}>
{value}
</span>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface LabelProps {
children: ReactNode
htmlFor?: string
required?: boolean
className?: string
}
export function Label({ children, htmlFor, required, className }: LabelProps) {
return (
<label
htmlFor={htmlFor}
className={cn(
'text-sm font-medium text-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
>
{children}
{required && <span className="text-destructive ml-1">*</span>}
</label>
)
}

View File

@@ -0,0 +1,40 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface LinkProps {
href: string
children: ReactNode
variant?: 'default' | 'muted' | 'accent' | 'destructive'
external?: boolean
className?: string
onClick?: (e: React.MouseEvent) => void
}
const variantClasses = {
default: 'text-foreground hover:text-primary underline-offset-4 hover:underline',
muted: 'text-muted-foreground hover:text-foreground underline-offset-4 hover:underline',
accent: 'text-accent hover:text-accent/80 underline-offset-4 hover:underline',
destructive: 'text-destructive hover:text-destructive/80 underline-offset-4 hover:underline',
}
export function Link({
href,
children,
variant = 'default',
external = false,
className,
onClick
}: LinkProps) {
const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {}
return (
<a
href={href}
className={cn('transition-colors duration-150', variantClasses[variant], className)}
onClick={onClick}
{...externalProps}
>
{children}
</a>
)
}

View File

@@ -0,0 +1,35 @@
import { ReactNode } from 'react'
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => ReactNode
emptyMessage?: string
className?: string
itemClassName?: string
}
export function List<T>({
items,
renderItem,
emptyMessage = 'No items to display',
className = '',
itemClassName = ''
}: ListProps<T>) {
if (items.length === 0) {
return (
<div className="text-center text-muted-foreground py-8">
{emptyMessage}
</div>
)
}
return (
<div className={className}>
{items.map((item, index) => (
<div key={index} className={itemClassName}>
{renderItem(item, index)}
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,32 @@
import { cn } from '@/lib/utils'
interface ListItemProps {
icon?: React.ReactNode
children: React.ReactNode
onClick?: () => void
active?: boolean
className?: string
endContent?: React.ReactNode
}
export function ListItem({ icon, children, onClick, active, className, endContent }: ListItemProps) {
const isInteractive = !!onClick
return (
<div
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors',
isInteractive && 'cursor-pointer hover:bg-accent',
active && 'bg-accent',
className
)}
onClick={onClick}
role={isInteractive ? 'button' : undefined}
tabIndex={isInteractive ? 0 : undefined}
>
{icon && <div className="flex-shrink-0 text-muted-foreground">{icon}</div>}
<div className="flex-1 min-w-0 text-sm">{children}</div>
{endContent && <div className="flex-shrink-0">{endContent}</div>}
</div>
)
}

View File

@@ -0,0 +1,49 @@
import { cn } from '@/lib/utils'
interface LiveIndicatorProps {
label?: string
showLabel?: boolean
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function LiveIndicator({
label = 'LIVE',
showLabel = true,
size = 'md',
className,
}: LiveIndicatorProps) {
const sizeClasses = {
sm: 'text-xs gap-1.5',
md: 'text-sm gap-2',
lg: 'text-base gap-2.5',
}
const dotSizeClasses = {
sm: 'w-2 h-2',
md: 'w-2.5 h-2.5',
lg: 'w-3 h-3',
}
return (
<div className={cn('inline-flex items-center font-medium', sizeClasses[size], className)}>
<span className="relative flex">
<span
className={cn(
'absolute inline-flex rounded-full bg-red-500 opacity-75 animate-ping',
dotSizeClasses[size]
)}
/>
<span
className={cn(
'relative inline-flex rounded-full bg-red-500',
dotSizeClasses[size]
)}
/>
</span>
{showLabel && (
<span className="text-red-500 font-bold tracking-wider">{label}</span>
)}
</div>
)
}

View File

@@ -0,0 +1,20 @@
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
const sizeClasses = {
sm: 'w-4 h-4 border-2',
md: 'w-6 h-6 border-2',
lg: 'w-8 h-8 border-3',
}
return (
<div
className={`inline-block ${sizeClasses[size]} border-primary border-t-transparent rounded-full animate-spin ${className}`}
role="status"
aria-label="Loading"
/>
)
}

View File

@@ -0,0 +1,31 @@
import { cn } from '@/lib/utils'
export interface LoadingStateProps {
message?: string
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function LoadingState({
message = 'Loading...',
size = 'md',
className
}: LoadingStateProps) {
const sizeClasses = {
sm: 'w-4 h-4 border-2',
md: 'w-8 h-8 border-3',
lg: 'w-12 h-12 border-4',
}
return (
<div className={cn('flex flex-col items-center justify-center gap-3 py-8', className)}>
<div className={cn(
'border-primary border-t-transparent rounded-full animate-spin',
sizeClasses[size]
)} />
{message && (
<p className="text-sm text-muted-foreground">{message}</p>
)}
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { cn } from '@/lib/utils'
import { TrendUp, TrendDown } from '@phosphor-icons/react'
interface MetricDisplayProps {
label: string
value: string | number
trend?: {
value: number
direction: 'up' | 'down'
}
icon?: React.ReactNode
className?: string
variant?: 'default' | 'primary' | 'accent'
}
export function MetricDisplay({
label,
value,
trend,
icon,
className,
variant = 'default'
}: MetricDisplayProps) {
const variantClasses = {
default: 'text-foreground',
primary: 'text-primary',
accent: 'text-accent',
}
return (
<div className={cn('flex flex-col gap-1', className)}>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{icon && <span className="text-muted-foreground">{icon}</span>}
{label}
</div>
<div className="flex items-baseline gap-2">
<span className={cn('text-2xl font-bold', variantClasses[variant])}>
{value}
</span>
{trend && (
<span className={cn(
'flex items-center gap-0.5 text-xs font-medium',
trend.direction === 'up' ? 'text-green-600 dark:text-green-400' : 'text-destructive'
)}>
{trend.direction === 'up' ? <TrendUp size={14} /> : <TrendDown size={14} />}
{Math.abs(trend.value)}%
</span>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
import { X } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface ModalProps {
isOpen: boolean
onClose: () => void
title?: string
children: React.ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
showCloseButton?: boolean
className?: string
}
export function Modal({
isOpen,
onClose,
title,
children,
size = 'md',
showCloseButton = true,
className,
}: ModalProps) {
if (!isOpen) return null
const sizeStyles = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
full: 'max-w-full m-4',
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm animate-in fade-in-0"
onClick={onClose}
>
<div
className={cn(
'relative w-full bg-card border border-border rounded-lg shadow-lg animate-in zoom-in-95',
sizeStyles[size],
className
)}
onClick={(e) => e.stopPropagation()}
>
{(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 modal"
>
<X className="w-5 h-5" />
</button>
)}
</div>
)}
<div className="p-6">{children}</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,67 @@
import { Info, CheckCircle, Warning, XCircle } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface NotificationProps {
type: 'info' | 'success' | 'warning' | 'error'
title: string
message?: string
onClose?: () => void
className?: string
}
export function Notification({ type, title, message, onClose, className }: NotificationProps) {
const config = {
info: {
icon: Info,
color: 'text-blue-500',
bg: 'bg-blue-500/10',
border: 'border-blue-500/20',
},
success: {
icon: CheckCircle,
color: 'text-accent',
bg: 'bg-accent/10',
border: 'border-accent/20',
},
warning: {
icon: Warning,
color: 'text-yellow-500',
bg: 'bg-yellow-500/10',
border: 'border-yellow-500/20',
},
error: {
icon: XCircle,
color: 'text-destructive',
bg: 'bg-destructive/10',
border: 'border-destructive/20',
},
}
const { icon: Icon, color, bg, border } = config[type]
return (
<div
className={cn(
'flex gap-3 p-4 rounded-lg border',
bg,
border,
className
)}
>
<Icon className={cn('w-5 h-5 flex-shrink-0', color)} weight="fill" />
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm">{title}</h4>
{message && <p className="text-sm text-muted-foreground mt-1">{message}</p>}
</div>
{onClose && (
<button
onClick={onClose}
className="flex-shrink-0 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Close notification"
>
<XCircle className="w-4 h-4" />
</button>
)}
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { cn } from '@/lib/utils'
interface BasicPageHeaderProps {
title: string
description?: string
actions?: React.ReactNode
className?: string
}
export function BasicPageHeader({ title, description, actions, className }: BasicPageHeaderProps) {
return (
<div className={cn('flex items-start justify-between mb-6', className)}>
<div className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
{actions && (
<div className="flex gap-2">{actions}</div>
)}
</div>
)
}

View File

@@ -0,0 +1,62 @@
import { cn } from '@/lib/utils'
interface ProgressBarProps {
value: number
max?: number
size?: 'sm' | 'md' | 'lg'
variant?: 'default' | 'accent' | 'destructive'
showLabel?: boolean
className?: string
}
const sizeClasses = {
sm: 'h-1',
md: 'h-2',
lg: 'h-3',
}
const variantClasses = {
default: 'bg-primary',
accent: 'bg-accent',
destructive: 'bg-destructive',
}
export function ProgressBar({
value,
max = 100,
size = 'md',
variant = 'default',
showLabel = false,
className
}: ProgressBarProps) {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
return (
<div className="w-full">
<div
className={cn(
'relative w-full bg-secondary rounded-full overflow-hidden',
sizeClasses[size],
className
)}
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
>
<div
className={cn(
'h-full transition-all duration-300 ease-out',
variantClasses[variant]
)}
style={{ width: `${percentage}%` }}
/>
</div>
{showLabel && (
<span className="text-xs text-muted-foreground mt-1 block">
{Math.round(percentage)}%
</span>
)}
</div>
)
}

View File

@@ -0,0 +1,56 @@
import { cn } from '@/lib/utils'
interface PulseProps {
variant?: 'primary' | 'accent' | 'success' | 'warning' | 'error'
size?: 'sm' | 'md' | 'lg'
speed?: 'slow' | 'normal' | 'fast'
className?: string
}
export function Pulse({
variant = 'primary',
size = 'md',
speed = 'normal',
className,
}: PulseProps) {
const sizeClasses = {
sm: 'w-2 h-2',
md: 'w-3 h-3',
lg: 'w-4 h-4',
}
const variantClasses = {
primary: 'bg-primary',
accent: 'bg-accent',
success: 'bg-green-500',
warning: 'bg-yellow-500',
error: 'bg-red-500',
}
const speedClasses = {
slow: 'animate-pulse [animation-duration:3s]',
normal: 'animate-pulse',
fast: 'animate-pulse [animation-duration:0.5s]',
}
return (
<div className={cn('relative inline-flex', className)}>
<span
className={cn(
'inline-flex rounded-full opacity-75',
sizeClasses[size],
variantClasses[variant],
speedClasses[speed]
)}
/>
<span
className={cn(
'absolute inline-flex rounded-full opacity-75',
sizeClasses[size],
variantClasses[variant],
speedClasses[speed]
)}
/>
</div>
)
}

View File

@@ -0,0 +1,69 @@
import { cn } from '@/lib/utils'
interface RadioOption {
value: string
label: string
disabled?: boolean
}
interface RadioGroupProps {
options: RadioOption[]
value: string
onChange: (value: string) => void
name: string
orientation?: 'horizontal' | 'vertical'
className?: string
}
export function RadioGroup({
options,
value,
onChange,
name,
orientation = 'vertical',
className
}: RadioGroupProps) {
return (
<div
role="radiogroup"
className={cn(
'flex gap-3',
orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap',
className
)}
>
{options.map((option) => (
<label
key={option.value}
className={cn(
'flex items-center gap-2 cursor-pointer',
option.disabled && 'opacity-50 cursor-not-allowed'
)}
>
<input
type="radio"
name={name}
value={option.value}
checked={value === option.value}
onChange={(e) => !option.disabled && onChange(e.target.value)}
disabled={option.disabled}
className="sr-only"
/>
<span
className={cn(
'w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors',
value === option.value
? 'border-primary bg-primary'
: 'border-input bg-background'
)}
>
{value === option.value && (
<span className="w-2 h-2 rounded-full bg-primary-foreground" />
)}
</span>
<span className="text-sm font-medium">{option.label}</span>
</label>
))}
</div>
)
}

View File

@@ -0,0 +1,71 @@
import { Star } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface RatingProps {
value: number
onChange?: (value: number) => void
max?: number
size?: 'sm' | 'md' | 'lg'
readonly?: boolean
showValue?: boolean
className?: string
}
export function Rating({
value,
onChange,
max = 5,
size = 'md',
readonly = false,
showValue = false,
className
}: RatingProps) {
const sizeStyles = {
sm: 16,
md: 20,
lg: 24,
}
const iconSize = sizeStyles[size]
return (
<div className={cn('flex items-center gap-2', className)}>
<div className="flex items-center gap-0.5">
{Array.from({ length: max }, (_, index) => {
const starValue = index + 1
const isFilled = starValue <= value
const isHalfFilled = starValue - 0.5 === value
return (
<button
key={index}
type="button"
onClick={() => !readonly && onChange?.(starValue)}
disabled={readonly}
className={cn(
'transition-colors',
!readonly && 'cursor-pointer hover:scale-110',
readonly && 'cursor-default'
)}
aria-label={`Rate ${starValue} out of ${max}`}
>
<Star
size={iconSize}
weight={isFilled ? 'fill' : 'regular'}
className={cn(
'transition-colors',
isFilled ? 'text-accent fill-accent' : 'text-muted'
)}
/>
</button>
)
})}
</div>
{showValue && (
<span className="text-sm font-medium text-muted-foreground">
{value.toFixed(1)} / {max}
</span>
)}
</div>
)
}

View File

@@ -0,0 +1,57 @@
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface GridProps {
children: ReactNode
columns?: 1 | 2 | 3 | 4 | 5 | 6
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
responsive?: boolean
className?: string
}
const columnClasses = {
1: 'grid-cols-1',
2: 'grid-cols-2',
3: 'grid-cols-3',
4: 'grid-cols-4',
5: 'grid-cols-5',
6: 'grid-cols-6',
}
const gapClasses = {
none: 'gap-0',
xs: 'gap-1',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
}
const responsiveClasses = {
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
}
export function ResponsiveGrid({
children,
columns = 3,
gap = 'md',
responsive = true,
className,
}: GridProps) {
return (
<div
className={cn(
'grid',
responsive && columns > 1 ? responsiveClasses[columns] : columnClasses[columns],
gapClasses[gap],
className
)}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,35 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
interface ScrollAreaProps {
children: ReactNode
className?: string
maxHeight?: string | number
}
export function ScrollArea({ children, className, maxHeight }: ScrollAreaProps) {
return (
<ScrollAreaPrimitive.Root
className={cn('relative overflow-hidden', className)}
style={{ maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar
className="flex touch-none select-none transition-colors p-0.5 bg-transparent hover:bg-muted"
orientation="vertical"
>
<ScrollAreaPrimitive.Thumb className="flex-1 bg-border rounded-full relative" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Scrollbar
className="flex touch-none select-none transition-colors p-0.5 bg-transparent hover:bg-muted"
orientation="horizontal"
>
<ScrollAreaPrimitive.Thumb className="flex-1 bg-border rounded-full relative" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}

View File

@@ -0,0 +1,46 @@
import { MagnifyingGlass, X } from '@phosphor-icons/react'
import { Input } from './Input'
interface BasicSearchInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
onClear?: () => void
className?: string
}
export function BasicSearchInput({
value,
onChange,
placeholder = 'Search...',
onClear,
className,
}: BasicSearchInputProps) {
const handleClear = () => {
onChange('')
onClear?.()
}
return (
<Input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={className}
leftIcon={<MagnifyingGlass size={18} />}
rightIcon={
value && (
<button
type="button"
onClick={handleClear}
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear search"
>
<X size={18} />
</button>
)
}
/>
)
}

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface SectionProps {
children: ReactNode
spacing?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
className?: string
}
const spacingClasses = {
none: '',
sm: 'py-4',
md: 'py-8',
lg: 'py-12',
xl: 'py-16',
}
export function Section({ children, spacing = 'md', className }: SectionProps) {
return (
<section className={cn(spacingClasses[spacing], className)}>
{children}
</section>
)
}

View File

@@ -0,0 +1,69 @@
import { cn } from '@/lib/utils'
interface SelectOption {
value: string
label: string
disabled?: boolean
}
interface SelectProps {
value: string
onChange: (value: string) => void
options: SelectOption[]
label?: string
placeholder?: string
error?: boolean
helperText?: string
disabled?: boolean
className?: string
}
export function Select({
value,
onChange,
options,
label,
placeholder = 'Select an option',
error,
helperText,
disabled,
className,
}: SelectProps) {
return (
<div className={cn('w-full', className)}>
{label && (
<label className="block text-sm font-medium mb-1.5 text-foreground">
{label}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
className={cn(
'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-colors',
error ? 'border-destructive focus-visible:ring-destructive' : 'border-input'
)}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
{helperText && (
<p className={cn('text-xs mt-1.5', error ? 'text-destructive' : 'text-muted-foreground')}>
{helperText}
</p>
)}
</div>
)
}

View File

@@ -0,0 +1,36 @@
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,
}}
/>
)
}

View File

@@ -0,0 +1,65 @@
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>
)
}

View File

@@ -0,0 +1,31 @@
import { cn } from '@/lib/utils'
interface SpacerProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
axis?: 'horizontal' | 'vertical' | 'both'
className?: string
}
const sizeClasses = {
xs: 1,
sm: 2,
md: 4,
lg: 8,
xl: 16,
'2xl': 24,
}
export function Spacer({ size = 'md', axis = 'vertical', className }: SpacerProps) {
const spacing = sizeClasses[size]
return (
<div
className={cn(className)}
style={{
width: axis === 'horizontal' || axis === 'both' ? `${spacing * 4}px` : undefined,
height: axis === 'vertical' || axis === 'both' ? `${spacing * 4}px` : undefined,
}}
aria-hidden="true"
/>
)
}

View File

@@ -0,0 +1,35 @@
import { cn } from '@/lib/utils'
import { Sparkle as SparkleIcon } from '@phosphor-icons/react'
interface SparkleProps {
variant?: 'default' | 'primary' | 'accent' | 'gold'
size?: number
animate?: boolean
className?: string
}
export function Sparkle({
variant = 'default',
size = 16,
animate = true,
className,
}: SparkleProps) {
const variantClasses = {
default: 'text-foreground',
primary: 'text-primary',
accent: 'text-accent',
gold: 'text-yellow-500',
}
return (
<SparkleIcon
size={size}
weight="fill"
className={cn(
variantClasses[variant],
animate && 'animate-pulse',
className
)}
/>
)
}

View File

@@ -0,0 +1,17 @@
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)}
/>
)
}

View File

@@ -0,0 +1,63 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface StackProps {
children: ReactNode
direction?: 'horizontal' | 'vertical'
spacing?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
align?: 'start' | 'center' | 'end' | 'stretch'
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
wrap?: boolean
className?: string
}
const spacingClasses = {
none: 'gap-0',
xs: 'gap-1',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
}
const alignClasses = {
start: 'items-start',
center: 'items-center',
end: 'items-end',
stretch: 'items-stretch',
}
const justifyClasses = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
around: 'justify-around',
evenly: 'justify-evenly',
}
export function Stack({
children,
direction = 'vertical',
spacing = 'md',
align = 'stretch',
justify = 'start',
wrap = false,
className
}: StackProps) {
return (
<div
className={cn(
'flex',
direction === 'horizontal' ? 'flex-row' : 'flex-col',
spacingClasses[spacing],
alignClasses[align],
justifyClasses[justify],
wrap && 'flex-wrap',
className
)}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,25 @@
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" />
}

View File

@@ -0,0 +1,67 @@
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>
)
}

View File

@@ -0,0 +1,67 @@
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>
)
}

View File

@@ -0,0 +1,16 @@
interface TabIconProps {
icon: React.ReactNode
variant?: 'default' | 'gradient'
}
export function TabIcon({ icon, variant = 'default' }: TabIconProps) {
if (variant === 'gradient') {
return (
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center text-primary shrink-0">
{icon}
</div>
)
}
return <>{icon}</>
}

View File

@@ -0,0 +1,84 @@
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>
)
}

View File

@@ -0,0 +1,67 @@
import { cn } from '@/lib/utils'
interface Tab {
id: string
label: string
icon?: React.ReactNode
disabled?: boolean
}
interface TabsProps {
tabs: Tab[]
activeTab: string
onChange: (tabId: string) => void
variant?: 'default' | 'pills' | 'underline'
className?: string
}
export function Tabs({ tabs, activeTab, onChange, variant = 'default', className }: TabsProps) {
const variantStyles = {
default: {
container: 'border-b border-border',
tab: 'border-b-2 border-transparent data-[active=true]:border-primary',
active: 'text-foreground',
inactive: 'text-muted-foreground hover:text-foreground',
},
pills: {
container: 'bg-muted p-1 rounded-lg',
tab: 'rounded-md data-[active=true]:bg-background data-[active=true]:shadow-sm',
active: 'text-foreground',
inactive: 'text-muted-foreground hover:text-foreground',
},
underline: {
container: 'border-b border-border',
tab: 'border-b-2 border-transparent data-[active=true]:border-accent',
active: 'text-accent',
inactive: 'text-muted-foreground hover:text-foreground',
},
}
const styles = variantStyles[variant]
return (
<div className={cn('flex gap-1', styles.container, className)}>
{tabs.map((tab) => {
const isActive = tab.id === activeTab
return (
<button
key={tab.id}
onClick={() => !tab.disabled && onChange(tab.id)}
disabled={tab.disabled}
data-active={isActive}
className={cn(
'flex items-center gap-2 px-4 py-2 font-medium text-sm transition-colors',
isActive ? styles.active : styles.inactive,
styles.tab,
tab.disabled && 'opacity-50 cursor-not-allowed'
)}
>
{tab.icon}
{tab.label}
</button>
)
})}
</div>
)
}

View File

@@ -0,0 +1,59 @@
import { X } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface TagProps {
children: React.ReactNode
variant?: 'default' | 'primary' | 'secondary' | 'accent' | 'destructive'
size?: 'sm' | 'md' | 'lg'
removable?: boolean
onRemove?: () => void
className?: string
}
export function Tag({
children,
variant = 'default',
size = 'md',
removable = false,
onRemove,
className
}: TagProps) {
const variantStyles = {
default: 'bg-muted text-muted-foreground',
primary: 'bg-primary/10 text-primary',
secondary: 'bg-secondary text-secondary-foreground',
accent: 'bg-accent/10 text-accent',
destructive: 'bg-destructive/10 text-destructive',
}
const sizeStyles = {
sm: 'text-xs px-2 py-0.5 gap-1',
md: 'text-sm px-3 py-1 gap-1.5',
lg: 'text-base px-4 py-1.5 gap-2',
}
return (
<span
className={cn(
'inline-flex items-center rounded-full font-medium transition-colors',
variantStyles[variant],
sizeStyles[size],
className
)}
>
{children}
{removable && onRemove && (
<button
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
className="hover:opacity-70 transition-opacity"
aria-label="Remove tag"
>
<X className="w-3 h-3" />
</button>
)}
</span>
)
}

View File

@@ -0,0 +1,22 @@
import { ReactNode } from 'react'
interface TextProps {
children: ReactNode
variant?: 'body' | 'caption' | 'muted' | 'small'
className?: string
}
const variantClasses = {
body: 'text-sm text-foreground',
caption: 'text-xs text-muted-foreground',
muted: 'text-sm text-muted-foreground',
small: 'text-xs text-foreground',
}
export function Text({ children, variant = 'body', className = '' }: TextProps) {
return (
<p className={`${variantClasses[variant]} ${className}`}>
{children}
</p>
)
}

View File

@@ -0,0 +1,42 @@
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: boolean
helperText?: string
label?: string
}
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ error, helperText, label, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium mb-1.5 text-foreground">
{label}
</label>
)}
<textarea
ref={ref}
className={cn(
'flex min-h-[80px] w-full rounded-md border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
'resize-vertical transition-colors',
error ? 'border-destructive focus-visible:ring-destructive' : 'border-input',
className
)}
{...props}
/>
{helperText && (
<p className={cn('text-xs mt-1.5', error ? 'text-destructive' : 'text-muted-foreground')}>
{helperText}
</p>
)}
</div>
)
}
)
TextArea.displayName = 'TextArea'

View File

@@ -0,0 +1,38 @@
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface TextGradientProps {
children: ReactNode
from?: string
to?: string
via?: string
direction?: 'to-r' | 'to-l' | 'to-b' | 'to-t' | 'to-br' | 'to-bl' | 'to-tr' | 'to-tl'
className?: string
animate?: boolean
}
export function TextGradient({
children,
from = 'from-primary',
to = 'to-accent',
via,
direction = 'to-r',
className,
animate = false,
}: TextGradientProps) {
const gradientClasses = cn(
'bg-gradient-to-r',
from,
via,
to,
direction !== 'to-r' && `bg-gradient-${direction}`,
'bg-clip-text text-transparent',
animate && 'animate-gradient-x'
)
return (
<span className={cn(gradientClasses, className)}>
{children}
</span>
)
}

View File

@@ -0,0 +1,27 @@
import { cn } from '@/lib/utils'
interface TextHighlightProps {
children: React.ReactNode
variant?: 'primary' | 'accent' | 'success' | 'warning' | 'error'
className?: string
}
export function TextHighlight({ children, variant = 'primary', className }: TextHighlightProps) {
const variantClasses = {
primary: 'bg-primary/10 text-primary border-primary/20',
accent: 'bg-accent/10 text-accent-foreground border-accent/20',
success: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20',
warning: 'bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-500/20',
error: 'bg-destructive/10 text-destructive border-destructive/20',
}
return (
<span className={cn(
'inline-flex items-center px-2 py-0.5 rounded border font-medium text-sm',
variantClasses[variant],
className
)}>
{children}
</span>
)
}

View File

@@ -0,0 +1,83 @@
import { cn } from '@/lib/utils'
interface TimelineItem {
title: string
description?: string
timestamp?: string
icon?: React.ReactNode
status?: 'completed' | 'current' | 'pending'
}
interface TimelineProps {
items: TimelineItem[]
className?: string
}
export function Timeline({ items, className }: TimelineProps) {
return (
<div className={cn('space-y-4', className)}>
{items.map((item, index) => {
const isLast = index === items.length - 1
const status = item.status || 'pending'
return (
<div key={index} className="flex gap-4">
<div className="flex flex-col items-center">
<div
className={cn(
'w-8 h-8 rounded-full flex items-center justify-center border-2 transition-colors',
status === 'completed' && 'bg-primary border-primary text-primary-foreground',
status === 'current' && 'bg-accent border-accent text-accent-foreground',
status === 'pending' && 'bg-background border-muted text-muted-foreground'
)}
>
{item.icon || (
<div
className={cn(
'w-2 h-2 rounded-full',
status === 'completed' && 'bg-primary-foreground',
status === 'current' && 'bg-accent-foreground',
status === 'pending' && 'bg-muted'
)}
/>
)}
</div>
{!isLast && (
<div
className={cn(
'w-0.5 flex-1 min-h-[40px] transition-colors',
status === 'completed' ? 'bg-primary' : 'bg-muted'
)}
/>
)}
</div>
<div className="flex-1 pb-8">
<div className="flex items-start justify-between gap-4">
<div>
<h4
className={cn(
'font-medium',
status === 'pending' && 'text-muted-foreground'
)}
>
{item.title}
</h4>
{item.description && (
<p className="text-sm text-muted-foreground mt-1">
{item.description}
</p>
)}
</div>
{item.timestamp && (
<span className="text-xs text-muted-foreground whitespace-nowrap">
{item.timestamp}
</span>
)}
</div>
</div>
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { format, formatDistanceToNow } from 'date-fns'
import { cn } from '@/lib/utils'
interface TimestampProps {
date: Date | number | string
relative?: boolean
formatString?: string
className?: string
}
export function Timestamp({
date,
relative = false,
formatString = 'MMM d, yyyy h:mm a',
className
}: TimestampProps) {
const dateObj = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date
const displayText = relative
? formatDistanceToNow(dateObj, { addSuffix: true })
: format(dateObj, formatString)
return (
<time
dateTime={dateObj.toISOString()}
className={cn('text-sm text-muted-foreground', className)}
>
{displayText}
</time>
)
}

View File

@@ -0,0 +1,65 @@
import { cn } from '@/lib/utils'
interface ToggleProps {
checked: boolean
onChange: (checked: boolean) => void
label?: string
disabled?: boolean
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function Toggle({
checked,
onChange,
label,
disabled = false,
size = 'md',
className
}: ToggleProps) {
const sizeStyles = {
sm: {
container: 'w-8 h-4',
thumb: 'w-3 h-3',
translate: 'translate-x-4',
},
md: {
container: 'w-11 h-6',
thumb: 'w-5 h-5',
translate: 'translate-x-5',
},
lg: {
container: 'w-14 h-7',
thumb: 'w-6 h-6',
translate: 'translate-x-7',
},
}
const { container, thumb, translate } = sizeStyles[size]
return (
<label className={cn('flex items-center gap-2 cursor-pointer', disabled && 'opacity-50 cursor-not-allowed', className)}>
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={cn(
'relative inline-flex items-center rounded-full transition-colors',
container,
checked ? 'bg-primary' : 'bg-input'
)}
>
<span
className={cn(
'inline-block rounded-full bg-background transition-transform',
thumb,
checked ? translate : 'translate-x-0.5'
)}
/>
</button>
{label && <span className="text-sm font-medium">{label}</span>}
</label>
)
}

View File

@@ -0,0 +1,11 @@
import { Tree } from '@phosphor-icons/react'
interface TreeIconProps {
size?: number
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
className?: string
}
export function TreeIcon({ size = 20, weight = 'duotone', className = '' }: TreeIconProps) {
return <Tree size={size} weight={weight} className={className} />
}

View File

@@ -1,50 +1,116 @@
export { ErrorBadge } from './ErrorBadge'
export { SeedDataStatus } from './SeedDataStatus'
// Auto-generated exports - DO NOT EDIT MANUALLY
export { ActionButton } from './ActionButton'
export { IconButton } from './IconButton'
export { StatusBadge } from './StatusBadge'
export { DataSourceBadge } from './DataSourceBadge'
export { BindingIndicator } from './BindingIndicator'
export { StatCard } from './StatCard'
export { EmptyState } from './EmptyState'
export { DetailRow } from './DetailRow'
export { CompletionCard } from './CompletionCard'
export { TipsCard } from './TipsCard'
export { CountBadge } from './CountBadge'
export { ConfirmButton } from './ConfirmButton'
export { FilterInput } from './FilterInput'
export { MetricCard } from './MetricCard'
export { Tooltip } from './Tooltip'
export { Image } from './Image'
export { FileUpload } from './FileUpload'
export { Popover } from './Popover'
export { Menu } from './Menu'
export { Accordion } from './Accordion'
export { Card } from './Card'
export { CopyButton } from './CopyButton'
export { PasswordInput } from './PasswordInput'
export { Button } from './Button'
export { ActionCard } from './ActionCard'
export { ActionIcon } from './ActionIcon'
export { Alert } from './Alert'
export { AppLogo } from './AppLogo'
export { Avatar } from './Avatar'
export { AvatarGroup } from './AvatarGroup'
export { Badge } from './Badge'
export { Switch } from './Switch'
export { Separator } from './Separator'
export { HoverCard } from './HoverCard'
export { BindingIndicator } from './BindingIndicator'
export { BreadcrumbNav as Breadcrumb, BreadcrumbNav } from './Breadcrumb'
export { Button } from './Button'
export { ButtonGroup } from './ButtonGroup'
export { Calendar } from './Calendar'
export { Card } from './Card'
export { Checkbox } from './Checkbox'
export { Chip } from './Chip'
export { CircularProgress } from './CircularProgress'
export { Code } from './Code'
export { ColorSwatch } from './ColorSwatch'
export { CommandPalette } from './CommandPalette'
export { CompletionCard } from './CompletionCard'
export { ConfirmButton } from './ConfirmButton'
export { Container } from './Container'
export { ContextMenu } from './ContextMenu'
export type { ContextMenuItemType } from './ContextMenu'
export { CopyButton } from './CopyButton'
export { CountBadge } from './CountBadge'
export { DataList } from './DataList'
export { DataSourceBadge } from './DataSourceBadge'
export { DataTable } from './DataTable'
export type { Column } from './DataTable'
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from './Form'
export { DatePicker } from './DatePicker'
export { RangeSlider } from './RangeSlider'
export { CircularProgress } from './CircularProgress'
export { NumberInput } from './NumberInput'
export { QuickActionButton } from './QuickActionButton'
export { PanelHeader } from './PanelHeader'
export { GlowCard } from './GlowCard'
export { ActionCard } from './ActionCard'
export { DetailRow } from './DetailRow'
export { Divider } from './Divider'
export { Dot } from './Dot'
export { Drawer } from './Drawer'
export { EmptyMessage } from './EmptyMessage'
export { EmptyState } from './EmptyState'
export { EmptyStateIcon } from './EmptyStateIcon'
export { ErrorBadge } from './ErrorBadge'
export { FileIcon } from './FileIcon'
export { FileUpload } from './FileUpload'
export { FilterInput } from './FilterInput'
export { Flex } from './Flex'
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from './Form'
export { GlowCard } from './GlowCard'
export { Grid } from './Grid'
export { Heading } from './Heading'
export { HelperText } from './HelperText'
export { HoverCard } from './HoverCard'
export { IconButton } from './IconButton'
export { IconText } from './IconText'
export { IconWrapper } from './IconWrapper'
export { Image } from './Image'
export { InfoBox } from './InfoBox'
export { InfoPanel } from './InfoPanel'
export { Input } from './Input'
export { Kbd } from './Kbd'
export { KeyValue } from './KeyValue'
export { Label } from './Label'
export { Link } from './Link'
export { List } from './List'
export { ListItem } from './ListItem'
export { LiveIndicator } from './LiveIndicator'
export { LoadingSpinner } from './LoadingSpinner'
export { LoadingState } from './LoadingState'
export { Menu } from './Menu'
export { MetricCard } from './MetricCard'
export { MetricDisplay } from './MetricDisplay'
export { Modal } from './Modal'
export { Notification } from './Notification'
export { NumberInput } from './NumberInput'
export { BasicPageHeader as PageHeader } from './PageHeader'
export { PanelHeader } from './PanelHeader'
export { PasswordInput } from './PasswordInput'
export { Popover } from './Popover'
export { ProgressBar } from './ProgressBar'
export { Pulse } from './Pulse'
export { QuickActionButton } from './QuickActionButton'
export { RadioGroup as Radio, RadioGroup } from './Radio'
export { RangeSlider } from './RangeSlider'
export { Rating } from './Rating'
export { ResponsiveGrid } from './ResponsiveGrid'
export { ScrollArea } from './ScrollArea'
export { BasicSearchInput as SearchInput, BasicSearchInput } from './SearchInput'
export { Section } from './Section'
export { SeedDataStatus } from './SeedDataStatus'
export { Select } from './Select'
export { Separator } from './Separator'
export { Skeleton } from './Skeleton'
export { Slider } from './Slider'
export { Spacer } from './Spacer'
export { Sparkle } from './Sparkle'
export { Spinner } from './Spinner'
export { Stack } from './Stack'
export { StatCard } from './StatCard'
export { StatusBadge } from './StatusBadge'
export { StatusIcon } from './StatusIcon'
export { StepIndicator } from './StepIndicator'
export { Stepper } from './Stepper'
export { Switch } from './Switch'
export { TabIcon } from './TabIcon'
export { Table } from './Table'
export { Tabs } from './Tabs'
export { Tag } from './Tag'
export { Text } from './Text'
export { TextArea } from './TextArea'
export { TextGradient } from './TextGradient'
export { TextHighlight } from './TextHighlight'
export { Timeline } from './Timeline'
export { Timestamp } from './Timestamp'
export { TipsCard } from './TipsCard'
export { Toggle } from './Toggle'
export { Tooltip } from './Tooltip'
export { TreeIcon } from './TreeIcon'

View File

@@ -0,0 +1,120 @@
export { AppLogo } from './AppLogo'
export { TabIcon } from './TabIcon'
export { StatusIcon } from './StatusIcon'
export { ErrorBadge } from './ErrorBadge'
export { IconWrapper } from './IconWrapper'
export { LoadingSpinner } from './LoadingSpinner'
export { EmptyStateIcon } from './EmptyStateIcon'
export { TreeIcon } from './TreeIcon'
export { FileIcon } from './FileIcon'
export { ActionIcon } from './ActionIcon'
export { SeedDataStatus } from './SeedDataStatus'
export { ActionButton } from './ActionButton'
export { IconButton } from './IconButton'
export { DataList } from './DataList'
export { StatusBadge } from './StatusBadge'
export { Text } from './Text'
export { Heading } from './Heading'
export { List } from './List'
export { Grid } from './Grid'
export { DataSourceBadge } from './DataSourceBadge'
export { BindingIndicator } from './BindingIndicator'
export { StatCard } from './StatCard'
export { LoadingState } from './LoadingState'
export { EmptyState } from './EmptyState'
export { DetailRow } from './DetailRow'
export { CompletionCard } from './CompletionCard'
export { TipsCard } from './TipsCard'
export { CountBadge } from './CountBadge'
export { ConfirmButton } from './ConfirmButton'
export { FilterInput } from './FilterInput'
export { BasicPageHeader } from './PageHeader'
export { MetricCard } from './MetricCard'
export { Link } from './Link'
export { Divider } from './Divider'
export { Avatar } from './Avatar'
export { Chip } from './Chip'
export { Code } from './Code'
export { Kbd } from './Kbd'
export { ProgressBar } from './ProgressBar'
export { Skeleton } from './Skeleton'
export { Tooltip } from './Tooltip'
export { Alert } from './Alert'
export { Spinner } from './Spinner'
export { Dot } from './Dot'
export { Image } from './Image'
export { Label } from './Label'
export { HelperText } from './HelperText'
export { Container } from './Container'
export { Section } from './Section'
export { Stack } from './Stack'
export { Spacer } from './Spacer'
export { Timestamp } from './Timestamp'
export { ScrollArea } from './ScrollArea'
export { Tag } from './Tag'
export { Breadcrumb, BreadcrumbNav } from './Breadcrumb'
export { IconText } from './IconText'
export { TextArea } from './TextArea'
export { Input } from './Input'
export { Toggle } from './Toggle'
export { RadioGroup } from './Radio'
export { Checkbox } from './Checkbox'
export { Slider } from './Slider'
export { ColorSwatch } from './ColorSwatch'
export { Stepper } from './Stepper'
export { Rating } from './Rating'
export { Timeline } from './Timeline'
export { FileUpload } from './FileUpload'
export { Popover } from './Popover'
export { Tabs } from './Tabs'
export { Menu } from './Menu'
export { Accordion } from './Accordion'
export { Card } from './Card'
export { Notification } from './Notification'
export { CopyButton } from './CopyButton'
export { PasswordInput } from './PasswordInput'
export { BasicSearchInput } from './SearchInput'
export { Select } from './Select'
export { Modal } from './Modal'
export { Drawer } from './Drawer'
export { Table } from './Table'
export { Button } from './Button'
export { Badge } from './Badge'
export { Switch } from './Switch'
export { Separator } from './Separator'
export { HoverCard } from './HoverCard'
export { Calendar } from './Calendar'
export { ButtonGroup } from './ButtonGroup'
export { CommandPalette } from './CommandPalette'
export { ContextMenu } from './ContextMenu'
export type { ContextMenuItemType } from './ContextMenu'
export { DataTable } from './DataTable'
export type { Column } from './DataTable'
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from './Form'
export { DatePicker } from './DatePicker'
export { RangeSlider } from './RangeSlider'
export { InfoPanel } from './InfoPanel'
export { ResponsiveGrid } from './ResponsiveGrid'
export { Flex } from './Flex'
export { CircularProgress } from './CircularProgress'
export { AvatarGroup } from './AvatarGroup'
export { NumberInput } from './NumberInput'
export { TextGradient } from './TextGradient'
export { Pulse } from './Pulse'
export { QuickActionButton } from './QuickActionButton'
export { PanelHeader } from './PanelHeader'
export { LiveIndicator } from './LiveIndicator'
export { Sparkle } from './Sparkle'
export { GlowCard } from './GlowCard'
export { TextHighlight } from './TextHighlight'
export { ActionCard } from './ActionCard'
export { InfoBox } from './InfoBox'
export { ListItem } from './ListItem'
export { MetricDisplay } from './MetricDisplay'
export { KeyValue } from './KeyValue'
export { EmptyMessage } from './EmptyMessage'
export { StepIndicator } from './StepIndicator'

View File

@@ -0,0 +1,32 @@
import { Button, Flex } from '@/components/atoms'
import { Info, Sparkle } from '@phosphor-icons/react'
interface EditorActionsProps {
onExplain: () => void
onImprove: () => void
}
export function EditorActions({ onExplain, onImprove }: EditorActionsProps) {
return (
<Flex gap="sm">
<Button
size="sm"
variant="ghost"
onClick={onExplain}
className="h-7 text-xs"
leftIcon={<Info size={14} />}
>
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={onImprove}
className="h-7 text-xs"
leftIcon={<Sparkle size={14} weight="duotone" />}
>
Improve
</Button>
</Flex>
)
}

View File

@@ -0,0 +1,40 @@
import { ProjectFile } from '@/types/project'
import { FileCode, X } from '@phosphor-icons/react'
import { Flex } from '@/components/atoms'
interface FileTabsProps {
files: ProjectFile[]
activeFileId: string | null
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
}
export function FileTabs({ files, activeFileId, onFileSelect, onFileClose }: FileTabsProps) {
return (
<Flex align="center" gap="xs">
{files.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</Flex>
)
}

View File

@@ -0,0 +1,56 @@
import { Suspense, lazy } from 'react'
const MonacoEditor = lazy(() =>
import('@monaco-editor/react').then(module => ({
default: module.default
}))
)
interface LazyInlineMonacoEditorProps {
height?: string
defaultLanguage?: string
language?: string
value?: string
onChange?: (value: string | undefined) => void
theme?: string
options?: any
}
function InlineMonacoEditorFallback() {
return (
<div className="flex items-center justify-center bg-muted/50 rounded-md" style={{ height: '300px' }}>
<div className="flex flex-col items-center gap-2">
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-xs text-muted-foreground">Loading editor...</p>
</div>
</div>
)
}
export function LazyInlineMonacoEditor({
height = '300px',
defaultLanguage,
language,
value,
onChange,
theme = 'vs-dark',
options = {}
}: LazyInlineMonacoEditorProps) {
return (
<Suspense fallback={<InlineMonacoEditorFallback />}>
<MonacoEditor
height={height}
defaultLanguage={defaultLanguage}
language={language}
value={value}
onChange={onChange}
theme={theme}
options={{
minimap: { enabled: false },
fontSize: 12,
...options
}}
/>
</Suspense>
)
}

View File

@@ -0,0 +1,54 @@
import { Suspense, lazy } from 'react'
import { ProjectFile } from '@/types/project'
const MonacoEditor = lazy(() =>
import('@monaco-editor/react').then(module => ({
default: module.default
}))
)
interface LazyMonacoEditorProps {
file: ProjectFile
onChange: (content: string) => void
}
function MonacoEditorFallback() {
return (
<div className="h-full w-full flex items-center justify-center bg-card">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-muted-foreground">Loading editor...</p>
</div>
</div>
)
}
export function LazyMonacoEditor({ file, onChange }: LazyMonacoEditorProps) {
return (
<Suspense fallback={<MonacoEditorFallback />}>
<MonacoEditor
height="100%"
language={file.language}
value={file.content}
onChange={(value) => onChange(value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</Suspense>
)
}
export function preloadMonacoEditor() {
console.log('[MONACO] 🎯 Preloading Monaco Editor')
import('@monaco-editor/react')
.then(() => console.log('[MONACO] ✅ Monaco Editor preloaded'))
.catch(err => console.warn('[MONACO] ⚠️ Monaco Editor preload failed:', err))
}

View File

@@ -0,0 +1,45 @@
import { Badge, Flex, Text, IconWrapper } from '@/components/atoms'
interface NavigationItemProps {
icon: React.ReactNode
label: string
isActive: boolean
badge?: number
onClick: () => void
}
export function NavigationItem({
icon,
label,
isActive,
badge,
onClick,
}: NavigationItemProps) {
return (
<button
onClick={onClick}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
isActive
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted text-foreground'
}`}
>
<IconWrapper
icon={icon}
size="md"
variant={isActive ? 'default' : 'muted'}
/>
<Text className="flex-1 text-left font-medium" variant="small">
{label}
</Text>
{badge !== undefined && badge > 0 && (
<Badge
variant={isActive ? 'secondary' : 'destructive'}
className="ml-auto"
>
{badge}
</Badge>
)}
</button>
)
}

View File

@@ -0,0 +1,23 @@
import { TabIcon } from '@/components/atoms'
interface PageHeaderContentProps {
title: string
icon: React.ReactNode
description?: string
}
export function PageHeaderContent({ title, icon, description }: PageHeaderContentProps) {
return (
<div className="flex items-center gap-3">
<TabIcon icon={icon} variant="gradient" />
<div className="min-w-0">
<h2 className="text-lg sm:text-xl font-bold truncate">{title}</h2>
{description && (
<p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">
{description}
</p>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
{
"type": "Stack",
"props": {
"direction": "vertical",
"spacing": "none",
"className": "border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4"
},
"conditional": {
"if": "data.tabInfo",
"then": [
{
"type": "PageHeaderContent",
"bindings": {
"title": {
"source": "data.tabInfo.title"
},
"icon": {
"source": "data.tabInfo.icon"
},
"description": {
"source": "data.tabInfo.description"
}
}
}
]
}
}

View File

@@ -0,0 +1,46 @@
{
"type": "div",
"props": {
"className": "flex-1 flex flex-col"
},
"children": [
{
"type": "CanvasRenderer",
"bindings": {
"components": {
"source": "data.components"
},
"selectedId": {
"source": "data.selectedId"
},
"hoveredId": {
"source": "data.hoveredId"
},
"draggedOverId": {
"source": "data.draggedOverId"
},
"dropPosition": {
"source": "data.dropPosition"
},
"onSelect": {
"source": "bindings.onSelect"
},
"onHover": {
"source": "bindings.onHover"
},
"onHoverEnd": {
"source": "bindings.onHoverEnd"
},
"onDragOver": {
"source": "bindings.onDragOver"
},
"onDragLeave": {
"source": "bindings.onDragLeave"
},
"onDrop": {
"source": "bindings.onDrop"
}
}
}
]
}

View File

@@ -0,0 +1,84 @@
{
"type": "Stack",
"props": {
"direction": "vertical",
"spacing": "none",
"className": "w-80 border-l border-border bg-card"
},
"children": [
{
"type": "div",
"props": {
"className": "flex-1 overflow-hidden"
},
"children": [
{
"type": "ComponentTree",
"bindings": {
"components": {
"source": "data.components"
},
"selectedId": {
"source": "data.selectedId"
},
"hoveredId": {
"source": "data.hoveredId"
},
"draggedOverId": {
"source": "data.draggedOverId"
},
"dropPosition": {
"source": "data.dropPosition"
},
"onSelect": {
"source": "bindings.onSelect"
},
"onHover": {
"source": "bindings.onHover"
},
"onHoverEnd": {
"source": "bindings.onHoverEnd"
},
"onDragStart": {
"source": "bindings.onDragStart"
},
"onDragOver": {
"source": "bindings.onDragOver"
},
"onDragLeave": {
"source": "bindings.onDragLeave"
},
"onDrop": {
"source": "bindings.onDrop"
}
}
}
]
},
{
"type": "Separator"
},
{
"type": "div",
"props": {
"className": "flex-1 overflow-hidden"
},
"children": [
{
"type": "PropertyEditor",
"bindings": {
"component": {
"source": "data.selectedComponent"
},
"onUpdate": {
"source": "bindings.onUpdate"
},
"onDelete": {
"source": "bindings.onDelete"
}
}
}
]
}
]
}

View File

@@ -0,0 +1,16 @@
{
"type": "div",
"props": {
"className": "w-64 border-r border-border bg-card"
},
"children": [
{
"type": "ComponentPalette",
"bindings": {
"onDragStart": {
"source": "bindings.onDragStart"
}
}
}
]
}

View File

@@ -0,0 +1,51 @@
{
"type": "div",
"props": {
"className": "flex items-center justify-between border-b px-6 py-4"
},
"children": [
{
"type": "div",
"props": {
"className": "flex flex-col gap-1"
},
"children": [
{
"type": "h1",
"props": {
"className": "text-2xl font-bold",
"children": "Test Page"
}
},
{
"type": "p",
"props": {
"className": "text-sm text-muted-foreground",
"children": "This is a pure JSON component"
}
}
]
},
{
"type": "div",
"props": {
"className": "flex gap-2"
},
"children": [
{
"type": "Button",
"props": {
"variant": "outline",
"children": "Cancel"
}
},
{
"type": "Button",
"props": {
"children": "Save"
}
}
]
}
]
}

View File

@@ -0,0 +1,122 @@
{
"type": "Flex",
"props": {
"gap": "xs",
"shrink": true,
"className": "shrink-0"
},
"children": [
{
"type": "ToolbarButton",
"props": {
"icon": "MagnifyingGlass",
"label": "Search (Ctrl+K)",
"data-search-trigger": true
},
"bindings": {
"onClick": {
"source": "bindings.onSearch"
}
}
},
{
"type": "div",
"props": {
"className": "relative"
},
"condition": {
"source": "data.showErrorButton"
},
"conditional": {
"if": "data.errorCount > 0 && bindings.onShowErrors",
"then": [
{
"type": "ToolbarButton",
"props": {
"icon": "Wrench",
"variant": "outline",
"className": "border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
},
"bindings": {
"label": {
"source": "data.errorCount",
"transform": "count => `${count} ${count === 1 ? 'Error' : 'Errors'}`"
},
"onClick": {
"source": "bindings.onShowErrors"
}
},
"children": [
{
"type": "ErrorBadge",
"props": {
"size": "sm"
},
"bindings": {
"count": {
"source": "data.errorCount"
}
}
}
]
}
]
}
},
{
"type": "ToolbarButton",
"props": {
"icon": "Eye",
"label": "Preview (Ctrl+P)",
"variant": "outline"
},
"condition": {
"source": "bindings.onPreview"
},
"bindings": {
"onClick": {
"source": "bindings.onPreview"
}
}
},
{
"type": "ToolbarButton",
"props": {
"icon": "Keyboard",
"label": "Keyboard Shortcuts (Ctrl+/)",
"variant": "ghost",
"className": "hidden sm:flex"
},
"bindings": {
"onClick": {
"source": "bindings.onShowShortcuts"
}
}
},
{
"type": "ToolbarButton",
"props": {
"icon": "Sparkle",
"label": "AI Generate (Ctrl+Shift+G)"
},
"bindings": {
"onClick": {
"source": "bindings.onGenerateAI"
}
}
},
{
"type": "ToolbarButton",
"props": {
"icon": "Download",
"label": "Export Project (Ctrl+E)",
"variant": "default"
},
"bindings": {
"onClick": {
"source": "bindings.onExport"
}
}
}
]
}

View File

@@ -32,7 +32,7 @@
"type": "kv",
"key": "app-counter",
"defaultValue": 0
},
}
],
"components": [
{

View File

@@ -16,6 +16,12 @@ export function useStaticDataSource<T = any>(defaultValue: T) {
return [defaultValue, () => {}, () => {}] as const
}
export function useComputedDataSource<T = any>(expression: string | (() => T), dependencies: string[] = []) {
// Simple implementation - in a real app this would evaluate the expression
const computedValue = typeof expression === 'function' ? expression() : expression
return [computedValue, () => {}, () => {}] as const
}
export function useMultipleDataSources(_sources: DataSourceConfig[]) {
return {}
}

View File

@@ -34,12 +34,11 @@ export interface DeprecatedComponentInfo {
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
const sourceRoots = jsonRegistry.sourceRoots ?? {}
// Note: import.meta.glob requires literal patterns, cannot use variables
// Disabled for now since we're using explicit glob imports below
const moduleMapsBySource = Object.fromEntries(
Object.entries(sourceRoots).map(([source, patterns]) => {
if (!patterns || patterns.length === 0) {
return [source, {}]
}
return [source, import.meta.glob(patterns, { eager: true })]
Object.entries(sourceRoots).map(([source]) => {
return [source, {}]
})
) as Record<string, Record<string, unknown>>

View File

@@ -20,6 +20,7 @@ export const jsonUIComponentTypes = [
"article",
"aspect-ratio",
"AspectRatio",
"AtomicLibraryShowcase",
"avatar",
"Avatar",
"AvatarGroup",
@@ -48,8 +49,10 @@ export const jsonUIComponentTypes = [
"Card, CardContent, CardHeader",
"Card, CardContent, CardHeader, CardTitle",
"CardContent",
"CardDescription",
"CardFooter",
"CardHeader",
"CardTitle",
"Carousel",
"Chart",
"Check",
@@ -63,6 +66,7 @@ export const jsonUIComponentTypes = [
"CircularProgress",
"Clock",
"Code",
"CodeEditor",
"CodeExplanationDialog",
"collapsible",
"Collapsible",
@@ -77,9 +81,12 @@ export const jsonUIComponentTypes = [
"ComponentPalette",
"ComponentPaletteItem",
"ComponentTree",
"ComponentTreeBuilder",
"ComponentTreeManager",
"ComponentTreeNode",
"ComponentTreeWrapper",
"ConfirmButton",
"ConflictResolutionPage",
"Container",
"ContextMenu",
"ContextMenu as ShadcnContextMenu,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuTrigger,\n ContextMenuSeparator,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,",
@@ -102,6 +109,8 @@ export const jsonUIComponentTypes = [
"Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle",
"div",
"Divider",
"DockerBuildDebugger",
"DocumentationView",
"Dot",
"Download",
"drawer",
@@ -116,8 +125,12 @@ export const jsonUIComponentTypes = [
"EmptyState",
"EmptyStateIcon",
"ErrorBadge",
"ErrorPanel",
"Eye",
"EyeOff",
"FaviconDesigner",
"FeatureIdeaCloud",
"FeatureToggleSettings",
"FileIcon",
"FileTabs",
"FileUpload",
@@ -152,6 +165,9 @@ export const jsonUIComponentTypes = [
"Input",
"input-otp",
"InputOtp",
"JSONComponentTreeManager",
"JSONLambdaDesigner",
"JSONModelDesigner",
"JSONUIShowcase",
"Kbd",
"KeyValue",
@@ -194,6 +210,8 @@ export const jsonUIComponentTypes = [
"Pagination",
"PanelHeader",
"PasswordInput",
"PersistenceDashboard",
"PersistenceExample",
"Plus",
"popover",
"Popover",
@@ -201,9 +219,11 @@ export const jsonUIComponentTypes = [
"progress",
"Progress",
"ProgressBar",
"ProjectDashboard",
"PropertyEditor",
"PropertyEditorField",
"Pulse",
"PWASettings",
"QuickActionButton",
"Radio",
"radio-group",
@@ -214,6 +234,7 @@ export const jsonUIComponentTypes = [
"resizable",
"Resizable",
"ResponsiveGrid",
"SassStylesShowcase",
"Save",
"SaveIndicator",
"SaveIndicatorWrapper",
@@ -262,6 +283,7 @@ export const jsonUIComponentTypes = [
"Stepper",
"StorageSettings",
"StorageSettingsWrapper",
"StyleDesigner",
"switch",
"Switch",
"Switch as ShadcnSwitch",
@@ -277,6 +299,9 @@ export const jsonUIComponentTypes = [
"tabs",
"Tabs",
"Tabs, TabsContent, TabsList, TabsTrigger",
"TabsContent",
"TabsList",
"TabsTrigger",
"Tag",
"Text",
"textarea",