mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-05-05 10:59:35 +00:00
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:
@@ -3,7 +3,8 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(grep:*)"
|
"Bash(grep:*)",
|
||||||
|
"Bash(wc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-59
@@ -822,16 +822,6 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -4766,12 +4756,6 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-from": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6987,15 +6971,6 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -7003,16 +6978,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/state-local": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -7073,30 +7038,6 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"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": {
|
"node_modules/three": {
|
||||||
"version": "0.175.0",
|
"version": "0.175.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Plus, Pencil, Trash, Copy, Download, Upload } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface ActionIconProps {
|
|
||||||
action: 'add' | 'edit' | 'delete' | 'copy' | 'download' | 'upload'
|
|
||||||
size?: number
|
|
||||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionIcon({ action, size = 16, weight = 'regular', className = '' }: ActionIconProps) {
|
|
||||||
const iconMap = {
|
|
||||||
add: Plus,
|
|
||||||
edit: Pencil,
|
|
||||||
delete: Trash,
|
|
||||||
copy: Copy,
|
|
||||||
download: Download,
|
|
||||||
upload: Upload,
|
|
||||||
}
|
|
||||||
|
|
||||||
const IconComponent = iconMap[action]
|
|
||||||
return <IconComponent size={size} weight={weight} className={className} />
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { Info, Warning, CheckCircle, XCircle } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AlertProps {
|
|
||||||
variant?: 'info' | 'warning' | 'success' | 'error'
|
|
||||||
title?: string
|
|
||||||
children: ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantConfig = {
|
|
||||||
info: {
|
|
||||||
icon: Info,
|
|
||||||
classes: 'bg-blue-50 border-blue-200 text-blue-900',
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
icon: Warning,
|
|
||||||
classes: 'bg-yellow-50 border-yellow-200 text-yellow-900',
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
icon: CheckCircle,
|
|
||||||
classes: 'bg-green-50 border-green-200 text-green-900',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
icon: XCircle,
|
|
||||||
classes: 'bg-red-50 border-red-200 text-red-900',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Alert({ variant = 'info', title, children, className }: AlertProps) {
|
|
||||||
const config = variantConfig[variant]
|
|
||||||
const Icon = config.icon
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex gap-3 p-4 rounded-lg border',
|
|
||||||
config.classes,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<Icon size={20} weight="bold" className="flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
{title && <div className="font-semibold mb-1">{title}</div>}
|
|
||||||
<div className="text-sm">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Code } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
export function AppLogo() {
|
|
||||||
return (
|
|
||||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shrink-0">
|
|
||||||
<Code size={20} weight="duotone" className="text-white sm:w-6 sm:h-6" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AvatarProps {
|
|
||||||
src?: string
|
|
||||||
alt?: string
|
|
||||||
fallback?: string
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
xs: 'w-6 h-6 text-xs',
|
|
||||||
sm: 'w-8 h-8 text-sm',
|
|
||||||
md: 'w-10 h-10 text-base',
|
|
||||||
lg: 'w-12 h-12 text-lg',
|
|
||||||
xl: 'w-16 h-16 text-xl',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Avatar({ src, alt, fallback, size = 'md', className }: AvatarProps) {
|
|
||||||
const initials = fallback || alt?.slice(0, 2).toUpperCase() || '?'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full bg-muted overflow-hidden',
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{src ? (
|
|
||||||
<img src={src} alt={alt} className="w-full h-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<span className="font-medium text-muted-foreground">{initials}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface AvatarGroupProps {
|
|
||||||
avatars: {
|
|
||||||
src?: string
|
|
||||||
alt: string
|
|
||||||
fallback: string
|
|
||||||
}[]
|
|
||||||
max?: number
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
xs: 'h-6 w-6 text-xs',
|
|
||||||
sm: 'h-8 w-8 text-xs',
|
|
||||||
md: 'h-10 w-10 text-sm',
|
|
||||||
lg: 'h-12 w-12 text-base',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AvatarGroup({
|
|
||||||
avatars,
|
|
||||||
max = 5,
|
|
||||||
size = 'md',
|
|
||||||
className,
|
|
||||||
}: AvatarGroupProps) {
|
|
||||||
const displayAvatars = avatars.slice(0, max)
|
|
||||||
const remainingCount = Math.max(avatars.length - max, 0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('flex -space-x-2', className)}>
|
|
||||||
{displayAvatars.map((avatar, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted overflow-hidden',
|
|
||||||
sizeClasses[size]
|
|
||||||
)}
|
|
||||||
title={avatar.alt}
|
|
||||||
>
|
|
||||||
{avatar.src ? (
|
|
||||||
<img src={avatar.src} alt={avatar.alt} className="h-full w-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<span className="font-medium text-foreground">{avatar.fallback}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted',
|
|
||||||
sizeClasses[size]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="font-medium text-foreground">+{remainingCount}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { CaretRight } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
|
||||||
label: string
|
|
||||||
href?: string
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BreadcrumbNavProps {
|
|
||||||
items?: BreadcrumbItem[]
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BreadcrumbNav({ items = [], className }: BreadcrumbNavProps) {
|
|
||||||
return (
|
|
||||||
<nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
const isLast = index === items.length - 1
|
|
||||||
const linkClassName = cn(
|
|
||||||
'text-sm transition-colors',
|
|
||||||
isLast ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex items-center gap-2">
|
|
||||||
{item.href ? (
|
|
||||||
<a href={item.href} onClick={item.onClick} className={linkClassName}>
|
|
||||||
{item.label}
|
|
||||||
</a>
|
|
||||||
) : item.onClick ? (
|
|
||||||
<button onClick={item.onClick} className={linkClassName}>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm',
|
|
||||||
isLast ? 'text-foreground font-medium' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!isLast && <CaretRight className="w-4 h-4 text-muted-foreground" />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Breadcrumb = BreadcrumbNav
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
|
|
||||||
interface ButtonGroupProps {
|
|
||||||
children: ReactNode
|
|
||||||
orientation?: 'horizontal' | 'vertical'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ButtonGroup({
|
|
||||||
children,
|
|
||||||
orientation = 'horizontal',
|
|
||||||
className,
|
|
||||||
}: ButtonGroupProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'inline-flex',
|
|
||||||
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
|
|
||||||
'[&>button]:rounded-none',
|
|
||||||
'[&>button:first-child]:rounded-l-md',
|
|
||||||
'[&>button:last-child]:rounded-r-md',
|
|
||||||
orientation === 'vertical' && '[&>button:first-child]:rounded-t-md [&>button:first-child]:rounded-l-none',
|
|
||||||
orientation === 'vertical' && '[&>button:last-child]:rounded-b-md [&>button:last-child]:rounded-r-none',
|
|
||||||
'[&>button:not(:last-child)]:border-r-0',
|
|
||||||
orientation === 'vertical' && '[&>button:not(:last-child)]:border-b-0 [&>button:not(:last-child)]:border-r',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { Check, Minus } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CheckboxProps {
|
|
||||||
checked: boolean
|
|
||||||
onChange: (checked: boolean) => void
|
|
||||||
label?: string
|
|
||||||
indeterminate?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Checkbox({
|
|
||||||
checked,
|
|
||||||
onChange,
|
|
||||||
label,
|
|
||||||
indeterminate = false,
|
|
||||||
disabled = false,
|
|
||||||
size = 'md',
|
|
||||||
className
|
|
||||||
}: CheckboxProps) {
|
|
||||||
const sizeStyles = {
|
|
||||||
sm: 'w-4 h-4',
|
|
||||||
md: 'w-5 h-5',
|
|
||||||
lg: 'w-6 h-6',
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconSize = {
|
|
||||||
sm: 12,
|
|
||||||
md: 16,
|
|
||||||
lg: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={cn('flex items-center gap-2 cursor-pointer', disabled && 'opacity-50 cursor-not-allowed', className)}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={indeterminate ? 'mixed' : checked}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => !disabled && onChange(!checked)}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center rounded border-2 transition-colors',
|
|
||||||
sizeStyles[size],
|
|
||||||
checked || indeterminate
|
|
||||||
? 'bg-primary border-primary text-primary-foreground'
|
|
||||||
: 'bg-background border-input hover:border-ring'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{indeterminate ? (
|
|
||||||
<Minus size={iconSize[size]} weight="bold" />
|
|
||||||
) : checked ? (
|
|
||||||
<Check size={iconSize[size]} weight="bold" />
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
{label && <span className="text-sm font-medium select-none">{label}</span>}
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { X } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ChipProps {
|
|
||||||
children: ReactNode
|
|
||||||
variant?: 'default' | 'primary' | 'accent' | 'muted'
|
|
||||||
size?: 'sm' | 'md'
|
|
||||||
onRemove?: () => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantClasses = {
|
|
||||||
default: 'bg-secondary text-secondary-foreground',
|
|
||||||
primary: 'bg-primary text-primary-foreground',
|
|
||||||
accent: 'bg-accent text-accent-foreground',
|
|
||||||
muted: 'bg-muted text-muted-foreground',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: 'px-2 py-0.5 text-xs',
|
|
||||||
md: 'px-3 py-1 text-sm',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Chip({
|
|
||||||
children,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'md',
|
|
||||||
onRemove,
|
|
||||||
className
|
|
||||||
}: ChipProps) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1 rounded-full font-medium',
|
|
||||||
variantClasses[variant],
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{onRemove && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="inline-flex items-center justify-center hover:bg-black/10 rounded-full transition-colors"
|
|
||||||
aria-label="Remove"
|
|
||||||
>
|
|
||||||
<X size={size === 'sm' ? 12 : 14} weight="bold" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface CodeProps {
|
|
||||||
children: ReactNode
|
|
||||||
inline?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Code({ children, inline = true, className }: CodeProps) {
|
|
||||||
if (inline) {
|
|
||||||
return (
|
|
||||||
<code
|
|
||||||
className={cn(
|
|
||||||
'px-1.5 py-0.5 rounded bg-muted text-foreground font-mono text-sm',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pre
|
|
||||||
className={cn(
|
|
||||||
'p-4 rounded-lg bg-muted text-foreground font-mono text-sm overflow-x-auto',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<code>{children}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { X } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface DrawerProps {
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
title?: string
|
|
||||||
children: React.ReactNode
|
|
||||||
position?: 'left' | 'right' | 'top' | 'bottom'
|
|
||||||
size?: 'sm' | 'md' | 'lg'
|
|
||||||
showCloseButton?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Drawer({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
position = 'right',
|
|
||||||
size = 'md',
|
|
||||||
showCloseButton = true,
|
|
||||||
className,
|
|
||||||
}: DrawerProps) {
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const positionStyles = {
|
|
||||||
left: 'left-0 top-0 h-full',
|
|
||||||
right: 'right-0 top-0 h-full',
|
|
||||||
top: 'top-0 left-0 w-full',
|
|
||||||
bottom: 'bottom-0 left-0 w-full',
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeStyles = {
|
|
||||||
sm: position === 'left' || position === 'right' ? 'w-64' : 'h-64',
|
|
||||||
md: position === 'left' || position === 'right' ? 'w-96' : 'h-96',
|
|
||||||
lg: position === 'left' || position === 'right' ? 'w-[600px]' : 'h-[600px]',
|
|
||||||
}
|
|
||||||
|
|
||||||
const slideAnimation = {
|
|
||||||
left: 'animate-in slide-in-from-left',
|
|
||||||
right: 'animate-in slide-in-from-right',
|
|
||||||
top: 'animate-in slide-in-from-top',
|
|
||||||
bottom: 'animate-in slide-in-from-bottom',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-in fade-in-0"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'fixed z-50 bg-card border border-border shadow-lg',
|
|
||||||
positionStyles[position],
|
|
||||||
sizeStyles[size],
|
|
||||||
slideAnimation[position],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(title || showCloseButton) && (
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
|
||||||
{title && <h2 className="text-lg font-semibold">{title}</h2>}
|
|
||||||
{showCloseButton && (
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="ml-auto p-1 rounded-md hover:bg-accent transition-colors"
|
|
||||||
aria-label="Close drawer"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="p-6 overflow-auto h-full">{children}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { ReactNode, createElement } from 'react'
|
|
||||||
|
|
||||||
interface HeadingProps {
|
|
||||||
children: ReactNode
|
|
||||||
level?: 1 | 2 | 3 | 4 | 5 | 6
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelClasses = {
|
|
||||||
1: 'text-4xl font-bold tracking-tight',
|
|
||||||
2: 'text-3xl font-semibold tracking-tight',
|
|
||||||
3: 'text-2xl font-semibold tracking-tight',
|
|
||||||
4: 'text-xl font-semibold',
|
|
||||||
5: 'text-lg font-medium',
|
|
||||||
6: 'text-base font-medium',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Heading({ children, level = 1, className = '' }: HeadingProps) {
|
|
||||||
return createElement(
|
|
||||||
`h${level}`,
|
|
||||||
{ className: `${levelClasses[level]} ${className}` },
|
|
||||||
children
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SkeletonProps {
|
|
||||||
variant?: 'text' | 'rectangular' | 'circular' | 'rounded'
|
|
||||||
width?: string | number
|
|
||||||
height?: string | number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantClasses = {
|
|
||||||
text: 'rounded h-4',
|
|
||||||
rectangular: 'rounded-none',
|
|
||||||
circular: 'rounded-full',
|
|
||||||
rounded: 'rounded-lg',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Skeleton({
|
|
||||||
variant = 'rectangular',
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
className
|
|
||||||
}: SkeletonProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'bg-muted animate-pulse',
|
|
||||||
variantClasses[variant],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width: typeof width === 'number' ? `${width}px` : width,
|
|
||||||
height: typeof height === 'number' ? `${height}px` : height,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SliderProps {
|
|
||||||
value: number
|
|
||||||
onChange: (value: number) => void
|
|
||||||
min?: number
|
|
||||||
max?: number
|
|
||||||
step?: number
|
|
||||||
label?: string
|
|
||||||
showValue?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Slider({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
min = 0,
|
|
||||||
max = 100,
|
|
||||||
step = 1,
|
|
||||||
label,
|
|
||||||
showValue = false,
|
|
||||||
disabled = false,
|
|
||||||
className
|
|
||||||
}: SliderProps) {
|
|
||||||
const percentage = ((value - min) / (max - min)) * 100
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full', className)}>
|
|
||||||
{(label || showValue) && (
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
{label && <span className="text-sm font-medium">{label}</span>}
|
|
||||||
{showValue && <span className="text-sm text-muted-foreground">{value}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
|
||||||
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',
|
|
||||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
||||||
disabled && 'opacity-50 cursor-not-allowed',
|
|
||||||
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5',
|
|
||||||
'[&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:rounded-full',
|
|
||||||
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-transform',
|
|
||||||
'[&::-webkit-slider-thumb]:hover:scale-110',
|
|
||||||
'[&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:bg-primary',
|
|
||||||
'[&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:rounded-full',
|
|
||||||
'[&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:transition-transform',
|
|
||||||
'[&::-moz-range-thumb]:hover:scale-110'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(to right, hsl(var(--primary)) 0%, hsl(var(--primary)) ${percentage}%, hsl(var(--secondary)) ${percentage}%, hsl(var(--secondary)) 100%)`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { CircleNotch } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SpinnerProps {
|
|
||||||
size?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Spinner({ size = 24, className }: SpinnerProps) {
|
|
||||||
return (
|
|
||||||
<CircleNotch
|
|
||||||
size={size}
|
|
||||||
weight="bold"
|
|
||||||
className={cn('animate-spin text-primary', className)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { CheckCircle, CloudCheck } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface StatusIconProps {
|
|
||||||
type: 'saved' | 'synced'
|
|
||||||
size?: number
|
|
||||||
animate?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusIcon({ type, size = 14, animate = false }: StatusIconProps) {
|
|
||||||
const baseClassName = type === 'saved' ? 'text-accent' : ''
|
|
||||||
const animateClassName = animate ? 'animate-in zoom-in duration-200' : ''
|
|
||||||
const className = [baseClassName, animateClassName].filter(Boolean).join(' ')
|
|
||||||
|
|
||||||
if (type === 'saved') {
|
|
||||||
return (
|
|
||||||
<CheckCircle
|
|
||||||
size={size}
|
|
||||||
weight="fill"
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CloudCheck size={size} weight="duotone" />
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Check } from '@phosphor-icons/react'
|
|
||||||
|
|
||||||
interface StepIndicatorProps {
|
|
||||||
steps: Array<{
|
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
}>
|
|
||||||
currentStep: string
|
|
||||||
completedSteps?: string[]
|
|
||||||
onStepClick?: (stepId: string) => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StepIndicator({
|
|
||||||
steps,
|
|
||||||
currentStep,
|
|
||||||
completedSteps = [],
|
|
||||||
onStepClick,
|
|
||||||
className
|
|
||||||
}: StepIndicatorProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn('flex items-center gap-2', className)}>
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const isCompleted = completedSteps.includes(step.id)
|
|
||||||
const isCurrent = step.id === currentStep
|
|
||||||
const isClickable = !!onStepClick
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={step.id} className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2',
|
|
||||||
isClickable && 'cursor-pointer'
|
|
||||||
)}
|
|
||||||
onClick={() => isClickable && onStepClick(step.id)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium transition-colors',
|
|
||||||
isCompleted && 'bg-accent text-accent-foreground',
|
|
||||||
isCurrent && !isCompleted && 'bg-primary text-primary-foreground',
|
|
||||||
!isCurrent && !isCompleted && 'bg-muted text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCompleted ? <Check size={16} weight="bold" /> : index + 1}
|
|
||||||
</div>
|
|
||||||
<span className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
isCurrent && 'text-foreground',
|
|
||||||
!isCurrent && 'text-muted-foreground'
|
|
||||||
)}>
|
|
||||||
{step.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div className={cn(
|
|
||||||
'w-8 h-0.5',
|
|
||||||
completedSteps.includes(steps[index + 1].id) ? 'bg-accent' : 'bg-border'
|
|
||||||
)} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Check } from '@phosphor-icons/react'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface Step {
|
|
||||||
label: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StepperProps {
|
|
||||||
steps: Step[]
|
|
||||||
currentStep: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Stepper({ steps, currentStep, className }: StepperProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full', className)}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const isCompleted = index < currentStep
|
|
||||||
const isCurrent = index === currentStep
|
|
||||||
const isLast = index === steps.length - 1
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex items-center flex-1">
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-colors',
|
|
||||||
isCompleted && 'bg-primary text-primary-foreground',
|
|
||||||
isCurrent && 'bg-primary text-primary-foreground ring-4 ring-primary/20',
|
|
||||||
!isCompleted && !isCurrent && 'bg-muted text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCompleted ? <Check weight="bold" /> : index + 1}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium',
|
|
||||||
(isCompleted || isCurrent) ? 'text-foreground' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{step.label}
|
|
||||||
</div>
|
|
||||||
{step.description && (
|
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">
|
|
||||||
{step.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isLast && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex-1 h-0.5 mx-4 transition-colors',
|
|
||||||
isCompleted ? 'bg-primary' : 'bg-muted'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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}</>
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface TableColumn<T> {
|
|
||||||
key: keyof T | string
|
|
||||||
header: string
|
|
||||||
render?: (item: T) => React.ReactNode
|
|
||||||
width?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableProps<T> {
|
|
||||||
data: T[]
|
|
||||||
columns: TableColumn<T>[]
|
|
||||||
onRowClick?: (item: T) => void
|
|
||||||
striped?: boolean
|
|
||||||
hoverable?: boolean
|
|
||||||
compact?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Table<T extends Record<string, any>>({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
onRowClick,
|
|
||||||
striped = false,
|
|
||||||
hoverable = true,
|
|
||||||
compact = false,
|
|
||||||
className,
|
|
||||||
}: TableProps<T>) {
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full overflow-auto', className)}>
|
|
||||||
<table className="w-full border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-border bg-muted/50">
|
|
||||||
{columns.map((column, index) => (
|
|
||||||
<th
|
|
||||||
key={index}
|
|
||||||
className={cn(
|
|
||||||
'text-left font-medium text-sm text-muted-foreground',
|
|
||||||
compact ? 'px-3 py-2' : 'px-4 py-3',
|
|
||||||
column.width && `w-[${column.width}]`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{column.header}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data.map((item, rowIndex) => (
|
|
||||||
<tr
|
|
||||||
key={rowIndex}
|
|
||||||
onClick={() => onRowClick?.(item)}
|
|
||||||
className={cn(
|
|
||||||
'border-b border-border transition-colors',
|
|
||||||
striped && rowIndex % 2 === 1 && 'bg-muted/30',
|
|
||||||
hoverable && 'hover:bg-muted/50',
|
|
||||||
onRowClick && 'cursor-pointer'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{columns.map((column, colIndex) => (
|
|
||||||
<td
|
|
||||||
key={colIndex}
|
|
||||||
className={cn(
|
|
||||||
'text-sm',
|
|
||||||
compact ? 'px-3 py-2' : 'px-4 py-3'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{column.render
|
|
||||||
? column.render(item)
|
|
||||||
: item[column.key as keyof T]}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{data.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
No data available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,11 @@
|
|||||||
export { AppLogo } from './AppLogo'
|
|
||||||
export { TabIcon } from './TabIcon'
|
|
||||||
export { StatusIcon } from './StatusIcon'
|
|
||||||
export { ErrorBadge } from './ErrorBadge'
|
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 { SeedDataStatus } from './SeedDataStatus'
|
||||||
export { ActionButton } from './ActionButton'
|
export { ActionButton } from './ActionButton'
|
||||||
export { IconButton } from './IconButton'
|
export { IconButton } from './IconButton'
|
||||||
export { DataList } from './DataList'
|
|
||||||
export { StatusBadge } from './StatusBadge'
|
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 { DataSourceBadge } from './DataSourceBadge'
|
||||||
export { BindingIndicator } from './BindingIndicator'
|
export { BindingIndicator } from './BindingIndicator'
|
||||||
export { StatCard } from './StatCard'
|
export { StatCard } from './StatCard'
|
||||||
export { LoadingState } from './LoadingState'
|
|
||||||
export { EmptyState } from './EmptyState'
|
export { EmptyState } from './EmptyState'
|
||||||
export { DetailRow } from './DetailRow'
|
export { DetailRow } from './DetailRow'
|
||||||
export { CompletionCard } from './CompletionCard'
|
export { CompletionCard } from './CompletionCard'
|
||||||
@@ -28,58 +13,18 @@ export { TipsCard } from './TipsCard'
|
|||||||
export { CountBadge } from './CountBadge'
|
export { CountBadge } from './CountBadge'
|
||||||
export { ConfirmButton } from './ConfirmButton'
|
export { ConfirmButton } from './ConfirmButton'
|
||||||
export { FilterInput } from './FilterInput'
|
export { FilterInput } from './FilterInput'
|
||||||
export { BasicPageHeader } from './PageHeader'
|
|
||||||
export { MetricCard } from './MetricCard'
|
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 { Tooltip } from './Tooltip'
|
||||||
export { Alert } from './Alert'
|
|
||||||
export { Spinner } from './Spinner'
|
|
||||||
export { Dot } from './Dot'
|
|
||||||
export { Image } from './Image'
|
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 { FileUpload } from './FileUpload'
|
||||||
export { Popover } from './Popover'
|
export { Popover } from './Popover'
|
||||||
export { Tabs } from './Tabs'
|
|
||||||
export { Menu } from './Menu'
|
export { Menu } from './Menu'
|
||||||
export { Accordion } from './Accordion'
|
export { Accordion } from './Accordion'
|
||||||
export { Card } from './Card'
|
export { Card } from './Card'
|
||||||
export { Notification } from './Notification'
|
|
||||||
export { CopyButton } from './CopyButton'
|
export { CopyButton } from './CopyButton'
|
||||||
export { PasswordInput } from './PasswordInput'
|
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 { Button } from './Button'
|
||||||
export { Badge } from './Badge'
|
export { Badge } from './Badge'
|
||||||
@@ -87,7 +32,6 @@ export { Switch } from './Switch'
|
|||||||
export { Separator } from './Separator'
|
export { Separator } from './Separator'
|
||||||
export { HoverCard } from './HoverCard'
|
export { HoverCard } from './HoverCard'
|
||||||
export { Calendar } from './Calendar'
|
export { Calendar } from './Calendar'
|
||||||
export { ButtonGroup } from './ButtonGroup'
|
|
||||||
export { CommandPalette } from './CommandPalette'
|
export { CommandPalette } from './CommandPalette'
|
||||||
export { ContextMenu } from './ContextMenu'
|
export { ContextMenu } from './ContextMenu'
|
||||||
export type { ContextMenuItemType } 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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from './Form'
|
||||||
export { DatePicker } from './DatePicker'
|
export { DatePicker } from './DatePicker'
|
||||||
export { RangeSlider } from './RangeSlider'
|
export { RangeSlider } from './RangeSlider'
|
||||||
export { InfoPanel } from './InfoPanel'
|
|
||||||
export { ResponsiveGrid } from './ResponsiveGrid'
|
|
||||||
export { Flex } from './Flex'
|
|
||||||
export { CircularProgress } from './CircularProgress'
|
export { CircularProgress } from './CircularProgress'
|
||||||
export { AvatarGroup } from './AvatarGroup'
|
|
||||||
export { NumberInput } from './NumberInput'
|
export { NumberInput } from './NumberInput'
|
||||||
export { TextGradient } from './TextGradient'
|
|
||||||
export { Pulse } from './Pulse'
|
|
||||||
export { QuickActionButton } from './QuickActionButton'
|
export { QuickActionButton } from './QuickActionButton'
|
||||||
export { PanelHeader } from './PanelHeader'
|
export { PanelHeader } from './PanelHeader'
|
||||||
export { LiveIndicator } from './LiveIndicator'
|
|
||||||
export { Sparkle } from './Sparkle'
|
|
||||||
export { GlowCard } from './GlowCard'
|
export { GlowCard } from './GlowCard'
|
||||||
|
|
||||||
export { TextHighlight } from './TextHighlight'
|
|
||||||
export { ActionCard } from './ActionCard'
|
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 { EmptyMessage } from './EmptyMessage'
|
||||||
export { StepIndicator } from './StepIndicator'
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,39 +1,20 @@
|
|||||||
export { AppBranding } from './AppBranding'
|
|
||||||
export { Breadcrumb } from './Breadcrumb'
|
export { Breadcrumb } from './Breadcrumb'
|
||||||
export { CanvasRenderer } from './CanvasRenderer'
|
export { CanvasRenderer } from './CanvasRenderer'
|
||||||
export { CodeExplanationDialog } from './CodeExplanationDialog'
|
export { CodeExplanationDialog } from './CodeExplanationDialog'
|
||||||
export { ComponentPalette } from './ComponentPalette'
|
export { ComponentPalette } from './ComponentPalette'
|
||||||
export { ComponentTree } from './ComponentTree'
|
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 { GitHubBuildStatus } from './GitHubBuildStatus'
|
||||||
export { LabelWithBadge } from './LabelWithBadge'
|
|
||||||
export { LazyInlineMonacoEditor } from './LazyInlineMonacoEditor'
|
|
||||||
export { LazyMonacoEditor, preloadMonacoEditor } from './LazyMonacoEditor'
|
|
||||||
export { LazyLineChart } from './LazyLineChart'
|
export { LazyLineChart } from './LazyLineChart'
|
||||||
export { LazyBarChart } from './LazyBarChart'
|
export { LazyBarChart } from './LazyBarChart'
|
||||||
export { LazyD3BarChart } from './LazyD3BarChart'
|
export { LazyD3BarChart } from './LazyD3BarChart'
|
||||||
export { StorageSettings } from './StorageSettings'
|
export { StorageSettings } from './StorageSettings'
|
||||||
export { LoadingFallback } from './LoadingFallback'
|
|
||||||
export { MonacoEditorPanel } from './MonacoEditorPanel'
|
|
||||||
export { NavigationGroupHeader } from './NavigationGroupHeader'
|
export { NavigationGroupHeader } from './NavigationGroupHeader'
|
||||||
export { NavigationItem } from './NavigationItem'
|
|
||||||
export { PageHeaderContent } from './PageHeaderContent'
|
|
||||||
export { PropertyEditor } from './PropertyEditor'
|
export { PropertyEditor } from './PropertyEditor'
|
||||||
export { SaveIndicator } from './SaveIndicator'
|
export { SaveIndicator } from './SaveIndicator'
|
||||||
export { SeedDataManager } from './SeedDataManager'
|
export { SeedDataManager } from './SeedDataManager'
|
||||||
export { SearchBar } from './SearchBar'
|
|
||||||
export { StatCard } from './StatCard'
|
|
||||||
export { ToolbarButton } from './ToolbarButton'
|
export { ToolbarButton } from './ToolbarButton'
|
||||||
export { TreeCard } from './TreeCard'
|
|
||||||
export { TreeFormDialog } from './TreeFormDialog'
|
export { TreeFormDialog } from './TreeFormDialog'
|
||||||
export { TreeListHeader } from './TreeListHeader'
|
|
||||||
export { DataCard } from './DataCard'
|
|
||||||
export { SearchInput } from './SearchInput'
|
export { SearchInput } from './SearchInput'
|
||||||
export { ActionBar } from './ActionBar'
|
|
||||||
export { DataSourceCard } from './DataSourceCard'
|
|
||||||
export { BindingEditor } from './BindingEditor'
|
export { BindingEditor } from './BindingEditor'
|
||||||
export { DataSourceEditorDialog } from './DataSourceEditorDialog'
|
export { DataSourceEditorDialog } from './DataSourceEditorDialog'
|
||||||
export { ComponentBindingDialog } from './ComponentBindingDialog'
|
export { ComponentBindingDialog } from './ComponentBindingDialog'
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user