Remove 123 simple TypeScript components now defined in JSON

Deleted files:
- 71 simple atoms (ActionIcon, Alert, AppLogo, Avatar, Badge, Chip, etc.)
- 21 simple molecules (ActionBar, AppBranding, DataCard, etc.)
- 8 simple organisms (EmptyCanvasState, PageHeader, SchemaEditorCanvas, etc.)
- 23 simple UI components (accordion, alert, button, card, etc.)

Changes:
- Created cleanup-simple-components.ts script to automate deletion
- Created update-index-exports.ts script to update index files
- Updated index.ts in atoms/, molecules/, organisms/ to remove deleted exports
- Installed npm dependencies

Remaining TypeScript components (kept for complexity):
- 46 atoms wrapping UI or with hooks
- 20 molecules with complex logic
- 6 organisms with state management
- 11 UI components with advanced features

Total: 317 components now have JSON definitions, 123 TypeScript files deleted (39% reduction)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 19:45:25 +00:00
parent cf74c35e0a
commit aa51074380
131 changed files with 433 additions and 5648 deletions

View File

@@ -3,7 +3,8 @@
"allow": [
"Bash(ls:*)",
"Bash(find:*)",
"Bash(grep:*)"
"Bash(grep:*)",
"Bash(wc:*)"
]
}
}

59
package-lock.json generated
View File

@@ -822,16 +822,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"license": "MIT"
@@ -4766,12 +4756,6 @@
"concat-map": "0.0.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/callsites": {
"version": "3.1.0",
"dev": true,
@@ -6987,15 +6971,6 @@
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"license": "BSD-3-Clause",
@@ -7003,16 +6978,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/state-local": {
"version": "1.0.7",
"license": "MIT"
@@ -7073,30 +7038,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
"version": "5.46.0",
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/three": {
"version": "0.175.0",
"license": "MIT"

View File

@@ -0,0 +1,115 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(__dirname, '..')
/**
* List of simple presentational components that can be safely deleted
* These were identified by the conversion script as having no hooks or complex logic
*/
const SIMPLE_COMPONENTS = {
atoms: [
'ActionIcon', 'Alert', 'AppLogo', 'Avatar', 'Breadcrumb', 'ButtonGroup',
'Chip', 'Code', 'ColorSwatch', 'Container', 'DataList', 'Divider', 'Dot',
'EmptyStateIcon', 'FileIcon', 'Flex', 'Grid', 'Heading', 'HelperText',
'IconText', 'IconWrapper', 'InfoBox', 'InfoPanel', 'Input', 'Kbd',
'KeyValue', 'Label', 'Link', 'List', 'ListItem', 'LiveIndicator',
'LoadingSpinner', 'LoadingState', 'MetricDisplay', 'PageHeader', 'Pulse',
'ResponsiveGrid', 'ScrollArea', 'SearchInput', 'Section', 'Skeleton',
'Spacer', 'Sparkle', 'Spinner', 'StatusIcon', 'TabIcon', 'Tag', 'Text',
'TextArea', 'TextGradient', 'TextHighlight', 'Timestamp', 'TreeIcon',
// Additional simple ones
'AvatarGroup', 'Checkbox', 'Drawer', 'Modal', 'Notification', 'ProgressBar',
'Radio', 'Rating', 'Select', 'Slider', 'Stack', 'StepIndicator', 'Stepper',
'Table', 'Tabs', 'Timeline', 'Toggle',
],
molecules: [
'ActionBar', 'AppBranding', 'DataCard', 'DataSourceCard', 'EditorActions',
'EditorToolbar', 'EmptyEditorState', 'EmptyState', 'FileTabs', 'LabelWithBadge',
'LazyInlineMonacoEditor', 'LazyMonacoEditor', 'LoadingFallback', 'LoadingState',
'MonacoEditorPanel', 'NavigationItem', 'PageHeaderContent', 'SearchBar',
'StatCard', 'TreeCard', 'TreeListHeader',
],
organisms: [
'EmptyCanvasState', 'PageHeader', 'SchemaEditorCanvas', 'SchemaEditorPropertiesPanel',
'SchemaEditorSidebar', 'SchemaEditorStatusBar', 'SchemaEditorToolbar', 'ToolbarActions',
],
ui: [
'aspect-ratio', 'avatar', 'badge', 'checkbox', 'collapsible', 'hover-card',
'input', 'label', 'popover', 'progress', 'radio-group', 'resizable',
'scroll-area', 'separator', 'skeleton', 'switch', 'textarea', 'toggle',
// Additional ones
'accordion', 'alert', 'button', 'card', 'tabs', 'tooltip',
],
}
interface DeletionResult {
deleted: string[]
kept: string[]
failed: string[]
}
/**
* Delete simple TypeScript components
*/
async function deleteSimpleComponents(): Promise<void> {
console.log('🧹 Cleaning up simple TypeScript components...\n')
const results: DeletionResult = {
deleted: [],
kept: [],
failed: [],
}
// Process each category
for (const [category, components] of Object.entries(SIMPLE_COMPONENTS)) {
console.log(`📂 Processing ${category}...`)
const baseDir = path.join(rootDir, `src/components/${category}`)
for (const component of components) {
const fileName = component.endsWith('.tsx') ? component : `${component}.tsx`
const filePath = path.join(baseDir, fileName)
try {
await fs.access(filePath)
await fs.unlink(filePath)
results.deleted.push(`${category}/${fileName}`)
console.log(` ✅ Deleted: ${fileName}`)
} catch (error: unknown) {
// File doesn't exist or couldn't be deleted
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
results.kept.push(`${category}/${fileName}`)
console.log(` ⏭️ Skipped: ${fileName} (not found)`)
} else {
results.failed.push(`${category}/${fileName}`)
console.log(` ❌ Failed: ${fileName}`)
}
}
}
console.log()
}
// Summary
console.log('📊 Summary:')
console.log(` Deleted: ${results.deleted.length} files`)
console.log(` Skipped: ${results.kept.length} files`)
console.log(` Failed: ${results.failed.length} files`)
if (results.failed.length > 0) {
console.log('\n❌ Failed deletions:')
results.failed.forEach(f => console.log(` - ${f}`))
}
console.log('\n✨ Cleanup complete!')
console.log('\n📝 Next steps:')
console.log(' 1. Update index.ts files to remove deleted exports')
console.log(' 2. Search for direct imports of deleted components')
console.log(' 3. Run build to check for errors')
console.log(' 4. Run tests to verify functionality')
}
deleteSimpleComponents().catch(console.error)

View File

@@ -0,0 +1,76 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(__dirname, '..')
/**
* Update index.ts files to remove exports for deleted components
*/
async function updateIndexFiles(): Promise<void> {
console.log('📝 Updating index.ts files...\n')
const directories = [
'src/components/atoms',
'src/components/molecules',
'src/components/organisms',
'src/components/ui',
]
for (const dir of directories) {
const indexPath = path.join(rootDir, dir, 'index.ts')
const dirPath = path.join(rootDir, dir)
console.log(`📂 Processing ${dir}/index.ts...`)
try {
// Read current index.ts
const indexContent = await fs.readFile(indexPath, 'utf-8')
const lines = indexContent.split('\n')
// Get list of existing .tsx files
const files = await fs.readdir(dirPath)
const existingComponents = new Set(
files
.filter(f => f.endsWith('.tsx') && f !== 'index.tsx')
.map(f => f.replace('.tsx', ''))
)
// Filter out exports for deleted components
const updatedLines = lines.filter(line => {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('//')) {
return true
}
// Check if it's an export line
const exportMatch = line.match(/export\s+(?:\{([^}]+)\}|.+)\s+from\s+['"]\.\/([^'"]+)['"]/)
if (!exportMatch) {
return true // Keep non-export lines
}
const componentName = exportMatch[2]
const exists = existingComponents.has(componentName)
if (!exists) {
console.log(` ❌ Removing export: ${componentName}`)
return false
}
return true
})
// Write updated index.ts
await fs.writeFile(indexPath, updatedLines.join('\n'))
console.log(` ✅ Updated ${dir}/index.ts\n`)
} catch (error) {
console.error(` ❌ Error processing ${dir}/index.ts:`, error)
}
}
console.log('✨ Index files updated!')
}
updateIndexFiles().catch(console.error)

View File

@@ -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} />
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

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

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

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

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

View File

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

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

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

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

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

@@ -1,58 +0,0 @@
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean
helperText?: string
label?: string
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ error, helperText, label, leftIcon, rightIcon, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium mb-1.5 text-foreground">
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
{leftIcon}
</div>
)}
<input
ref={ref}
className={cn(
'flex h-10 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',
'transition-colors',
error ? 'border-destructive focus-visible:ring-destructive' : 'border-input',
leftIcon && 'pl-10',
rightIcon && 'pr-10',
className
)}
{...props}
/>
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
{rightIcon}
</div>
)}
</div>
{helperText && (
<p className={cn('text-xs mt-1.5', error ? 'text-destructive' : 'text-muted-foreground')}>
{helperText}
</p>
)}
</div>
)
}
)
Input.displayName = 'Input'

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -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,
}}
/>
)
}

View File

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

View File

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

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

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

View File

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

@@ -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" />
}

View File

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

View File

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

View File

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

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

View File

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

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

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

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

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

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

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

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

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

@@ -1,11 +0,0 @@
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,26 +1,11 @@
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'
@@ -28,58 +13,18 @@ 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'
@@ -87,7 +32,6 @@ 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'
@@ -96,25 +40,11 @@ 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

@@ -1,44 +0,0 @@
import { ReactNode } from 'react'
import { Button, Flex, Heading } from '@/components/atoms'
interface ActionBarProps {
title?: string
actions?: {
label: string
icon?: ReactNode
onClick: () => void
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
disabled?: boolean
}[]
children?: ReactNode
className?: string
}
export function ActionBar({ title, actions = [], children, className = '' }: ActionBarProps) {
return (
<Flex justify="between" align="center" gap="md" className={className}>
{title && (
<Heading level={2} className="text-xl font-semibold">
{title}
</Heading>
)}
{children}
{actions.length > 0 && (
<Flex gap="sm">
{actions.map((action, index) => (
<Button
key={index}
variant={action.variant || 'default'}
onClick={action.onClick}
disabled={action.disabled}
size="sm"
leftIcon={action.icon}
>
{action.label}
</Button>
))}
</Flex>
)}
</Flex>
)
}

View File

@@ -1,23 +0,0 @@
import { AppLogo, Stack, Heading, Text } from '@/components/atoms'
interface AppBrandingProps {
title?: string
subtitle?: string
}
export function AppBranding({
title = 'CodeForge',
subtitle = 'Low-Code Next.js App Builder'
}: AppBrandingProps) {
return (
<Stack direction="horizontal" align="center" spacing="sm" className="flex-1 min-w-0">
<AppLogo />
<Stack direction="vertical" spacing="none" className="min-w-[100px]">
<Heading level={1} className="text-base sm:text-xl font-bold whitespace-nowrap">{title}</Heading>
<Text variant="caption" className="hidden sm:block whitespace-nowrap">
{subtitle}
</Text>
</Stack>
</Stack>
)
}

View File

@@ -1,74 +0,0 @@
import { Card, Stack, Text, Heading, Skeleton, Flex, IconWrapper } from '@/components/atoms'
interface DataCardProps {
title?: string
value: string | number
description?: string
icon?: React.ReactNode
trend?: {
value: number
label: string
positive?: boolean
}
isLoading?: boolean
className?: string
}
export function DataCard({
title,
value,
description,
icon,
trend,
isLoading = false,
className = ''
}: DataCardProps) {
if (isLoading) {
return (
<Card className={className}>
<div className="pt-6 px-6 pb-6">
<Stack spacing="sm">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-24" />
</Stack>
</div>
</Card>
)
}
return (
<Card className={className}>
<div className="pt-6 px-6 pb-6">
<Flex justify="between" align="start" gap="md">
<Stack spacing="xs" className="flex-1">
{title && (
<Text variant="muted" className="font-medium">
{title}
</Text>
)}
<Heading level={1} className="text-3xl font-bold">
{value}
</Heading>
{description && (
<Text variant="caption">
{description}
</Text>
)}
{trend && (
<Text
variant="small"
className={trend.positive ? 'text-green-500' : 'text-red-500'}
>
{trend.positive ? '↑' : '↓'} {trend.value} {trend.label}
</Text>
)}
</Stack>
{icon && (
<IconWrapper icon={icon} size="lg" variant="muted" />
)}
</Flex>
</div>
</Card>
)
}

View File

@@ -1,69 +0,0 @@
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
import { DataSource } from '@/types/json-ui'
import { Pencil, Trash } from '@phosphor-icons/react'
interface DataSourceCardProps {
dataSource: DataSource
dependents?: DataSource[]
onEdit: (id: string) => void
onDelete: (id: string) => void
}
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
const renderTypeSpecificInfo = () => {
if (dataSource.type === 'kv') {
return (
<Text variant="caption" className="font-mono bg-muted/30 px-2 py-1 rounded">
Key: {dataSource.key || 'Not set'}
</Text>
)
}
return null
}
return (
<Card className="bg-card/50 backdrop-blur hover:bg-card/70 transition-colors">
<div className="p-4">
<Flex justify="between" align="start" gap="md">
<Stack spacing="sm" className="flex-1 min-w-0">
<Flex align="center" gap="sm">
<DataSourceBadge type={dataSource.type} />
<Text variant="small" className="font-mono font-medium truncate">
{dataSource.id}
</Text>
</Flex>
{renderTypeSpecificInfo()}
{dependents.length > 0 && (
<div className="pt-2 border-t border-border/50">
<Text variant="caption">
Used by {dependents.length} dependent {dependents.length === 1 ? 'source' : 'sources'}
</Text>
</div>
)}
</Stack>
<Flex align="center" gap="xs">
<IconButton
icon={<Pencil className="w-4 h-4" />}
variant="ghost"
size="sm"
onClick={() => onEdit(dataSource.id)}
/>
<IconButton
icon={<Trash className="w-4 h-4" />}
variant="ghost"
size="sm"
onClick={() => onDelete(dataSource.id)}
className="text-destructive hover:text-destructive"
disabled={dependents.length > 0}
/>
</Flex>
</Flex>
</div>
</Card>
)
}

View File

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

@@ -1,46 +0,0 @@
import { ProjectFile } from '@/types/project'
import { FileTabs } from './FileTabs'
import { EditorActions } from './EditorActions'
import { Flex } from '@/components/atoms'
interface EditorToolbarProps {
openFiles: ProjectFile[]
activeFileId: string | null
activeFile: ProjectFile | undefined
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
onExplain: () => void
onImprove: () => void
}
export function EditorToolbar({
openFiles,
activeFileId,
activeFile,
onFileSelect,
onFileClose,
onExplain,
onImprove,
}: EditorToolbarProps) {
return (
<Flex
align="center"
justify="between"
gap="xs"
className="bg-secondary/50 border-b border-border px-2 py-1"
>
<FileTabs
files={openFiles}
activeFileId={activeFileId}
onFileSelect={onFileSelect}
onFileClose={onFileClose}
/>
{activeFile && (
<EditorActions
onExplain={onExplain}
onImprove={onImprove}
/>
)}
</Flex>
)
}

View File

@@ -1,13 +0,0 @@
import { EmptyStateIcon, Stack, Text } from '@/components/atoms'
import { FileCode } from '@phosphor-icons/react'
export function EmptyEditorState() {
return (
<div className="flex-1 flex items-center justify-center">
<Stack direction="vertical" align="center" spacing="md">
<EmptyStateIcon icon={<FileCode size={48} />} />
<Text variant="muted">Select a file to edit</Text>
</Stack>
</div>
)
}

View File

@@ -1,29 +0,0 @@
import { EmptyStateIcon, Stack, Heading, Text } from '@/components/atoms'
interface EmptyStateProps {
icon: React.ReactNode
title: string
description?: string
action?: React.ReactNode
}
export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
return (
<Stack
direction="vertical"
align="center"
justify="center"
spacing="md"
className="py-12 px-4 text-center"
>
<EmptyStateIcon icon={icon} />
<Stack direction="vertical" spacing="sm">
<Heading level={3} className="text-lg">{title}</Heading>
{description && (
<Text variant="muted" className="max-w-md">{description}</Text>
)}
</Stack>
{action && <div className="mt-2">{action}</div>}
</Stack>
)
}

View File

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

@@ -1,24 +0,0 @@
import { Badge, Flex, Text } from '@/components/atoms'
interface LabelWithBadgeProps {
label: string
badge?: number | string
badgeVariant?: 'default' | 'secondary' | 'destructive' | 'outline'
}
export function LabelWithBadge({
label,
badge,
badgeVariant = 'secondary'
}: LabelWithBadgeProps) {
return (
<Flex align="center" gap="sm">
<Text variant="small" className="font-medium">{label}</Text>
{badge !== undefined && (
<Badge variant={badgeVariant} className="text-xs">
{badge}
</Badge>
)}
</Flex>
)
}

View File

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

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

@@ -1,16 +0,0 @@
import { LoadingSpinner } from '@/components/atoms'
interface LoadingFallbackProps {
message?: string
}
export function LoadingFallback({ message = 'Loading...' }: LoadingFallbackProps) {
return (
<div className="flex items-center justify-center h-full w-full">
<div className="flex flex-col items-center gap-3">
<LoadingSpinner />
<p className="text-sm text-muted-foreground">{message}</p>
</div>
</div>
)
}

View File

@@ -1,15 +0,0 @@
import { LoadingSpinner } from '@/components/atoms'
interface LoadingStateProps {
message?: string
size?: 'sm' | 'md' | 'lg'
}
export function LoadingState({ message = 'Loading...', size = 'md' }: LoadingStateProps) {
return (
<div className="flex flex-col items-center justify-center gap-3 py-12">
<LoadingSpinner size={size} />
<p className="text-sm text-muted-foreground">{message}</p>
</div>
)
}

View File

@@ -1,11 +0,0 @@
import { ProjectFile } from '@/types/project'
import { LazyMonacoEditor } from './LazyMonacoEditor'
interface MonacoEditorPanelProps {
file: ProjectFile
onChange: (content: string) => void
}
export function MonacoEditorPanel({ file, onChange }: MonacoEditorPanelProps) {
return <LazyMonacoEditor file={file} onChange={onChange} />
}

View File

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

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

@@ -1,36 +0,0 @@
import { Input, IconButton, Flex } from '@/components/atoms'
import { MagnifyingGlass, X } from '@phosphor-icons/react'
interface SearchBarProps {
value: string
onChange: (value: string) => void
placeholder?: string
className?: string
}
export function SearchBar({ value, onChange, placeholder = 'Search...', className }: SearchBarProps) {
return (
<Flex gap="sm" className={className}>
<div className="relative flex-1">
<MagnifyingGlass
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
size={16}
/>
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="pl-9"
/>
</div>
{value && (
<IconButton
icon={<X size={16} />}
variant="ghost"
onClick={() => onChange('')}
title="Clear search"
/>
)}
</Flex>
)
}

View File

@@ -1,32 +0,0 @@
import { Card, IconWrapper, Stack, Text } from '@/components/atoms'
interface StatCardProps {
icon: React.ReactNode
label: string
value: string | number
variant?: 'default' | 'primary' | 'destructive'
}
export function StatCard({ icon, label, value, variant = 'default' }: StatCardProps) {
const variantClasses = {
default: 'border-border',
primary: 'border-primary/50 bg-primary/5',
destructive: 'border-destructive/50 bg-destructive/5',
}
return (
<Card className={`p-4 ${variantClasses[variant]}`}>
<Stack direction="horizontal" align="center" spacing="md">
<IconWrapper
icon={icon}
size="lg"
variant={variant === 'default' ? 'muted' : variant}
/>
<Stack direction="vertical" spacing="xs" className="flex-1">
<Text variant="caption">{label}</Text>
<Text className="text-2xl font-bold">{value}</Text>
</Stack>
</Stack>
</Card>
)
}

View File

@@ -1,75 +0,0 @@
import { Card, Badge, ActionIcon, IconButton, Stack, Flex, Text, Heading } from '@/components/atoms'
import { ComponentTree } from '@/types/project'
interface TreeCardProps {
tree: ComponentTree
isSelected: boolean
onSelect: () => void
onEdit: () => void
onDuplicate: () => void
onDelete: () => void
disableDelete?: boolean
}
export function TreeCard({
tree,
isSelected,
onSelect,
onEdit,
onDuplicate,
onDelete,
disableDelete = false,
}: TreeCardProps) {
return (
<Card
className={`cursor-pointer transition-all p-4 ${
isSelected ? 'ring-2 ring-primary bg-accent' : 'hover:bg-accent/50'
}`}
onClick={onSelect}
>
<Stack spacing="sm">
<Flex justify="between" align="start" gap="sm">
<Stack spacing="xs" className="flex-1 min-w-0">
<Heading level={4} className="text-sm truncate">{tree.name}</Heading>
{tree.description && (
<Text variant="caption" className="line-clamp-2">
{tree.description}
</Text>
)}
<div>
<Badge variant="outline" className="text-xs">
{tree.rootNodes.length} components
</Badge>
</div>
</Stack>
</Flex>
<div onClick={(e) => e.stopPropagation()}>
<Flex gap="xs" className="mt-1">
<IconButton
icon={<ActionIcon action="edit" size={14} />}
variant="ghost"
size="sm"
onClick={onEdit}
title="Edit tree"
/>
<IconButton
icon={<ActionIcon action="copy" size={14} />}
variant="ghost"
size="sm"
onClick={onDuplicate}
title="Duplicate tree"
/>
<IconButton
icon={<ActionIcon action="delete" size={14} />}
variant="ghost"
size="sm"
onClick={onDelete}
disabled={disableDelete}
title="Delete tree"
/>
</Flex>
</div>
</Stack>
</Card>
)
}

View File

@@ -1,53 +0,0 @@
import { Button, TreeIcon, ActionIcon, Flex, Heading, Stack, IconButton } from '@/components/atoms'
interface TreeListHeaderProps {
onCreateNew: () => void
onImportJson: () => void
onExportJson: () => void
hasSelectedTree?: boolean
}
export function TreeListHeader({
onCreateNew,
onImportJson,
onExportJson,
hasSelectedTree = false,
}: TreeListHeaderProps) {
return (
<Stack spacing="sm">
<Flex justify="between" align="center">
<Flex align="center" gap="sm">
<TreeIcon size={20} />
<Heading level={2} className="text-lg font-semibold">Component Trees</Heading>
</Flex>
<IconButton
icon={<ActionIcon action="add" size={16} />}
size="sm"
onClick={onCreateNew}
/>
</Flex>
<Flex gap="sm">
<Button
size="sm"
variant="outline"
onClick={onImportJson}
className="flex-1 text-xs"
leftIcon={<ActionIcon action="upload" size={14} />}
>
Import JSON
</Button>
<Button
size="sm"
variant="outline"
onClick={onExportJson}
disabled={!hasSelectedTree}
className="flex-1 text-xs"
leftIcon={<ActionIcon action="download" size={14} />}
>
Export JSON
</Button>
</Flex>
</Stack>
)
}

View File

@@ -1,39 +1,20 @@
export { AppBranding } from './AppBranding'
export { Breadcrumb } from './Breadcrumb'
export { CanvasRenderer } from './CanvasRenderer'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { ComponentPalette } from './ComponentPalette'
export { ComponentTree } from './ComponentTree'
export { EditorActions } from './EditorActions'
export { EditorToolbar } from './EditorToolbar'
export { EmptyEditorState } from './EmptyEditorState'
export { FileTabs } from './FileTabs'
export { GitHubBuildStatus } from './GitHubBuildStatus'
export { LabelWithBadge } from './LabelWithBadge'
export { LazyInlineMonacoEditor } from './LazyInlineMonacoEditor'
export { LazyMonacoEditor, preloadMonacoEditor } from './LazyMonacoEditor'
export { LazyLineChart } from './LazyLineChart'
export { LazyBarChart } from './LazyBarChart'
export { LazyD3BarChart } from './LazyD3BarChart'
export { StorageSettings } from './StorageSettings'
export { LoadingFallback } from './LoadingFallback'
export { MonacoEditorPanel } from './MonacoEditorPanel'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { NavigationItem } from './NavigationItem'
export { PageHeaderContent } from './PageHeaderContent'
export { PropertyEditor } from './PropertyEditor'
export { SaveIndicator } from './SaveIndicator'
export { SeedDataManager } from './SeedDataManager'
export { SearchBar } from './SearchBar'
export { StatCard } from './StatCard'
export { ToolbarButton } from './ToolbarButton'
export { TreeCard } from './TreeCard'
export { TreeFormDialog } from './TreeFormDialog'
export { TreeListHeader } from './TreeListHeader'
export { DataCard } from './DataCard'
export { SearchInput } from './SearchInput'
export { ActionBar } from './ActionBar'
export { DataSourceCard } from './DataSourceCard'
export { BindingEditor } from './BindingEditor'
export { DataSourceEditorDialog } from './DataSourceEditorDialog'
export { ComponentBindingDialog } from './ComponentBindingDialog'

View File

@@ -1,38 +0,0 @@
import { Plus, Folder } from '@phosphor-icons/react'
import { EmptyState, ActionButton, Stack } from '@/components/atoms'
interface EmptyCanvasStateProps {
onAddFirstComponent?: () => void
onImportSchema?: () => void
}
export function EmptyCanvasState({ onAddFirstComponent, onImportSchema }: EmptyCanvasStateProps) {
return (
<div className="h-full flex flex-col items-center justify-center p-8 bg-muted/20">
<EmptyState
icon={<Folder size={64} weight="duotone" />}
title="Empty Canvas"
description="Start building your UI by dragging components from the left panel, or import an existing schema."
>
<Stack direction="horizontal" spacing="md" className="mt-4">
{onImportSchema && (
<ActionButton
icon={<Folder size={16} />}
label="Import Schema"
onClick={onImportSchema}
variant="outline"
/>
)}
{onAddFirstComponent && (
<ActionButton
icon={<Plus size={16} />}
label="Add Component"
onClick={onAddFirstComponent}
variant="default"
/>
)}
</Stack>
</EmptyState>
</div>
)
}

View File

@@ -1,27 +0,0 @@
import { PageHeaderContent } from '@/components/molecules'
import { Stack, Container } from '@/components/atoms'
import { tabInfo } from '@/lib/navigation-config'
interface PageHeaderProps {
activeTab: string
}
export function PageHeader({ activeTab }: PageHeaderProps) {
const info = tabInfo[activeTab]
if (!info) return null
return (
<Stack
direction="vertical"
spacing="none"
className="border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4"
>
<PageHeaderContent
title={info.title}
icon={info.icon}
description={info.description}
/>
</Stack>
)
}

View File

@@ -1,48 +0,0 @@
import { CanvasRenderer } from '@/components/molecules/CanvasRenderer'
import { UIComponent } from '@/types/json-ui'
interface SchemaEditorCanvasProps {
components: UIComponent[]
selectedId: string | null
hoveredId: string | null
draggedOverId: string | null
dropPosition: 'before' | 'after' | 'inside' | null
onSelect: (id: string | null) => void
onHover: (id: string | null) => void
onHoverEnd: () => void
onDragOver: (id: string, e: React.DragEvent) => void
onDragLeave: (e: React.DragEvent) => void
onDrop: (targetId: string, e: React.DragEvent) => void
}
export function SchemaEditorCanvas({
components,
selectedId,
hoveredId,
draggedOverId,
dropPosition,
onSelect,
onHover,
onHoverEnd,
onDragOver,
onDragLeave,
onDrop,
}: SchemaEditorCanvasProps) {
return (
<div className="flex-1 flex flex-col">
<CanvasRenderer
components={components}
selectedId={selectedId}
hoveredId={hoveredId}
draggedOverId={draggedOverId}
dropPosition={dropPosition}
onSelect={onSelect}
onHover={onHover}
onHoverEnd={onHoverEnd}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
/>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More