Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
2c0dfd9e7c Convert FeatureToggleSettings to JSON-driven component
Demonstrates that components with hooks and complex logic can be JSON-driven:
- Converted 153 lines of React/TSX to JSON schema + integration code
- UI structure now in feature-toggle-settings.json schema
- Custom hook logic preserved in TypeScript for type safety
- Shows how JSON can handle loops, events, conditional styling
- Business logic separated from presentation

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 19:22:38 +00:00
copilot-swe-agent[bot]
96744a6ab4 Initial plan 2026-01-18 18:55:54 +00:00
279 changed files with 3370 additions and 7226 deletions

View File

@@ -3,27 +3,7 @@
"allow": [
"Bash(ls:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(wc:*)",
"Bash(for file in accordion alert aspect-ratio avatar badge button card checkbox collapsible dialog hover-card input label popover progress radio-group resizable scroll-area separator skeleton sheet switch tabs textarea toggle tooltip)",
"Bash(do)",
"Bash([ -f \"src/config/pages/ui/$file.json\" ])",
"Bash(echo:*)",
"Bash(done)",
"Bash(for file in data-source-card editor-toolbar empty-editor-state monaco-editor-panel search-bar)",
"Bash([ -f \"src/config/pages/molecules/$file.json\" ])",
"Bash(for file in empty-canvas-state page-header schema-editor-canvas schema-editor-properties-panel schema-editor-sidebar schema-editor-status-bar schema-editor-toolbar toolbar-actions)",
"Bash([ -f \"src/config/pages/organisms/$file.json\" ])",
"Bash([ -f \"src/config/pages/atoms/input.json\" ])",
"Bash(npm run tsx:*)",
"Bash(npx tsx:*)",
"Bash(npm run test:e2e:*)",
"Bash(npx playwright:*)",
"Bash(timeout 15 npm run dev:*)",
"Bash(netstat:*)",
"Bash(findstr:*)",
"Bash(taskkill:*)",
"Bash(xargs:*)"
"Bash(grep:*)"
]
}
}

View File

@@ -8,10 +8,7 @@ test.describe('CodeForge - Core Functionality', () => {
})
test('should load the application successfully', async ({ page }) => {
// Check root has children (content rendered)
await page.waitForSelector('#root > *', { timeout: 10000 })
const root = page.locator('#root')
await expect(root).toHaveCount(1)
await expect(page.locator('body')).toBeVisible()
})
test('should display main navigation', async ({ page }) => {
@@ -53,8 +50,8 @@ test.describe('CodeForge - Responsive Design', () => {
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 10000 })
await page.waitForLoadState('networkidle', { timeout: 5000 })
await page.waitForSelector('#root > *', { timeout: 10000 })
await expect(page.locator('body')).toBeVisible()
})
test('should work on tablet viewport', async ({ page }) => {
@@ -62,7 +59,7 @@ test.describe('CodeForge - Responsive Design', () => {
await page.setViewportSize({ width: 768, height: 1024 })
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 10000 })
await page.waitForLoadState('networkidle', { timeout: 5000 })
await page.waitForSelector('#root > *', { timeout: 10000 })
await expect(page.locator('body')).toBeVisible()
})
})

View File

@@ -1,41 +0,0 @@
import { test } from '@playwright/test'
test('debug page load', async ({ page }) => {
const errors: string[] = []
const pageErrors: Error[] = []
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text())
}
})
page.on('pageerror', (error) => {
pageErrors.push(error)
})
await page.goto('/', { waitUntil: 'networkidle', timeout: 15000 })
// Wait a bit
await page.waitForTimeout(2000)
// Get page content
const rootHTML = await page.locator('#root').innerHTML().catch(() => 'ERROR GETTING ROOT')
console.log('=== PAGE ERRORS ===')
pageErrors.forEach(err => console.log(err.message))
console.log('\n=== CONSOLE ERRORS ===')
errors.forEach(err => console.log(err))
console.log('\n=== ROOT CONTENT ===')
console.log(rootHTML.substring(0, 500))
console.log('\n=== ROOT VISIBLE ===')
const rootVisible = await page.locator('#root').isVisible().catch(() => false)
console.log('Root visible:', rootVisible)
console.log('\n=== ROOT HAS CHILDREN ===')
const childCount = await page.locator('#root > *').count()
console.log('Child count:', childCount)
})

View File

@@ -4,12 +4,8 @@ test.describe('CodeForge - Smoke Tests', () => {
test('app loads successfully', async ({ page }) => {
test.setTimeout(20000)
await page.goto('/', { waitUntil: 'networkidle', timeout: 15000 })
// Check that the app has rendered content (more reliable than checking visibility)
const root = page.locator('#root')
await expect(root).toHaveCount(1, { timeout: 5000 })
// Wait for any content to be rendered
await page.waitForSelector('#root > *', { timeout: 10000 })
await expect(page.locator('body')).toBeVisible({ timeout: 5000 })
})
test('can navigate to dashboard tab', async ({ page }) => {

File diff suppressed because it is too large Load Diff

59
package-lock.json generated
View File

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

View File

@@ -1,75 +0,0 @@
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, '..')
const componentsToAnalyze = {
molecules: ['DataSourceCard', 'EditorToolbar', 'EmptyEditorState', 'MonacoEditorPanel', 'SearchBar'],
organisms: ['EmptyCanvasState', 'PageHeader', 'SchemaEditorCanvas', 'SchemaEditorPropertiesPanel',
'SchemaEditorSidebar', 'SchemaEditorStatusBar', 'SchemaEditorToolbar', 'ToolbarActions'],
}
async function analyzeComponent(category: string, component: string): Promise<void> {
const tsFile = path.join(rootDir, `src/components/${category}/${component}.tsx`)
const content = await fs.readFile(tsFile, 'utf-8')
// Check if it's pure composition (only uses UI primitives)
const hasBusinessLogic = /useState|useEffect|useCallback|useMemo|useReducer|useRef/.test(content)
const hasComplexLogic = /if\s*\(.*\{|switch\s*\(|for\s*\(|while\s*\(/.test(content)
// Extract what it imports
const imports = content.match(/import\s+\{[^}]+\}\s+from\s+['"][^'"]+['"]/g) || []
const importedComponents = imports.flatMap(imp => {
const match = imp.match(/\{([^}]+)\}/)
return match ? match[1].split(',').map(s => s.trim()) : []
})
// Check if it only imports from ui/atoms (pure composition)
const onlyUIPrimitives = imports.every(imp =>
imp.includes('@/components/ui/') ||
imp.includes('@/components/atoms/') ||
imp.includes('@/lib/utils') ||
imp.includes('lucide-react') ||
imp.includes('@phosphor-icons')
)
const lineCount = content.split('\n').length
console.log(`\n📄 ${component}`)
console.log(` Lines: ${lineCount}`)
console.log(` Has hooks: ${hasBusinessLogic ? '❌' : '✅'}`)
console.log(` Has complex logic: ${hasComplexLogic ? '❌' : '✅'}`)
console.log(` Only UI primitives: ${onlyUIPrimitives ? '✅' : '❌'}`)
console.log(` Imports: ${importedComponents.slice(0, 5).join(', ')}${importedComponents.length > 5 ? '...' : ''}`)
if (!hasBusinessLogic && onlyUIPrimitives && lineCount < 100) {
console.log(` 🎯 CANDIDATE FOR PURE JSON`)
}
}
async function main() {
console.log('🔍 Analyzing components for pure JSON conversion...\n')
console.log('Looking for components that:')
console.log(' - No hooks (useState, useEffect, etc.)')
console.log(' - No complex logic')
console.log(' - Only import UI primitives')
console.log(' - Are simple compositions\n')
for (const [category, components] of Object.entries(componentsToAnalyze)) {
console.log(`\n═══ ${category.toUpperCase()} ═══`)
for (const component of components) {
try {
await analyzeComponent(category, component)
} catch (e) {
console.log(`\n📄 ${component}`)
console.log(` ⚠️ Could not analyze: ${e}`)
}
}
}
console.log('\n\n✨ Analysis complete!')
}
main().catch(console.error)

View File

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

View File

@@ -1,262 +0,0 @@
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, '..')
interface ConversionConfig {
sourceDir: string
targetDir: string
category: 'atoms' | 'molecules' | 'organisms' | 'ui'
}
interface ComponentAnalysis {
name: string
hasHooks: boolean
hasComplexLogic: boolean
wrapsUIComponent: boolean
uiComponentName?: string
defaultProps: Record<string, unknown>
isSimplePresentational: boolean
}
/**
* Analyze a TypeScript component file to determine conversion strategy
*/
async function analyzeComponent(filePath: string): Promise<ComponentAnalysis> {
const content = await fs.readFile(filePath, 'utf-8')
const fileName = path.basename(filePath, '.tsx')
// Check for hooks
const hasHooks = /use[A-Z]\w+\(/.test(content) ||
/useState|useEffect|useCallback|useMemo|useRef|useReducer/.test(content)
// Check for complex logic
const hasComplexLogic = hasHooks ||
/switch\s*\(/.test(content) ||
/for\s*\(/.test(content) ||
/while\s*\(/.test(content) ||
content.split('\n').length > 100
// Check if it wraps a shadcn/ui component
const uiImportMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]@\/components\/ui\//)
const wrapsUIComponent = !!uiImportMatch
const uiComponentName = wrapsUIComponent ? uiImportMatch?.[1].trim() : undefined
// Extract default props from interface
const defaultProps: Record<string, unknown> = {}
const propDefaults = content.matchAll(/(\w+)\s*[?]?\s*:\s*([^=\n]+)\s*=\s*['"]?([^'";\n,}]+)['"]?/g)
for (const match of propDefaults) {
const [, propName, , defaultValue] = match
if (propName && defaultValue) {
defaultProps[propName] = defaultValue.replace(/['"]/g, '')
}
}
// Determine if it's simple presentational
const isSimplePresentational = !hasComplexLogic &&
!hasHooks &&
content.split('\n').length < 60
return {
name: fileName,
hasHooks,
hasComplexLogic,
wrapsUIComponent,
uiComponentName,
defaultProps,
isSimplePresentational,
}
}
/**
* Generate JSON definition for a component based on analysis
*/
function generateJSON(analysis: ComponentAnalysis, category: string): object {
// If it wraps a UI component, reference that
if (analysis.wrapsUIComponent && analysis.uiComponentName) {
return {
type: analysis.uiComponentName,
props: analysis.defaultProps,
}
}
// If it's simple presentational, create a basic structure
if (analysis.isSimplePresentational) {
return {
type: analysis.name,
props: analysis.defaultProps,
}
}
// If it has hooks or complex logic, mark as needing wrapper
if (analysis.hasHooks || analysis.hasComplexLogic) {
return {
type: analysis.name,
jsonCompatible: false,
wrapperRequired: true,
load: {
path: `@/components/${category}/${analysis.name}`,
export: analysis.name,
},
props: analysis.defaultProps,
metadata: {
notes: analysis.hasHooks ? 'Contains hooks - needs wrapper' : 'Complex logic - needs wrapper',
},
}
}
// Default case
return {
type: analysis.name,
props: analysis.defaultProps,
}
}
/**
* Convert a single TypeScript file to JSON
*/
async function convertFile(
sourceFile: string,
targetDir: string,
category: string
): Promise<{ success: boolean; analysis: ComponentAnalysis }> {
try {
const analysis = await analyzeComponent(sourceFile)
const json = generateJSON(analysis, category)
// Generate kebab-case filename
const jsonFileName = analysis.name
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, '') + '.json'
const targetFile = path.join(targetDir, jsonFileName)
await fs.writeFile(targetFile, JSON.stringify(json, null, 2) + '\n')
return { success: true, analysis }
} catch (error) {
console.error(`Error converting ${sourceFile}:`, error)
return {
success: false,
analysis: {
name: path.basename(sourceFile, '.tsx'),
hasHooks: false,
hasComplexLogic: false,
wrapsUIComponent: false,
defaultProps: {},
isSimplePresentational: false,
}
}
}
}
/**
* Convert all components in a directory
*/
async function convertDirectory(config: ConversionConfig): Promise<void> {
const sourceDir = path.join(rootDir, config.sourceDir)
const targetDir = path.join(rootDir, config.targetDir)
console.log(`\n📂 Converting ${config.category} components...`)
console.log(` Source: ${sourceDir}`)
console.log(` Target: ${targetDir}`)
// Ensure target directory exists
await fs.mkdir(targetDir, { recursive: true })
// Get all TypeScript files
const files = await fs.readdir(sourceDir)
const tsxFiles = files.filter(f => f.endsWith('.tsx') && !f.includes('.test.') && !f.includes('.stories.'))
console.log(` Found ${tsxFiles.length} TypeScript files\n`)
const results = {
total: 0,
simple: 0,
needsWrapper: 0,
wrapsUI: 0,
failed: 0,
}
// Convert each file
for (const file of tsxFiles) {
const sourceFile = path.join(sourceDir, file)
const { success, analysis } = await convertFile(sourceFile, targetDir, config.category)
results.total++
if (!success) {
results.failed++
console.log(`${file}`)
continue
}
if (analysis.wrapsUIComponent) {
results.wrapsUI++
console.log(` 🎨 ${file}${analysis.name} (wraps UI)`)
} else if (analysis.isSimplePresentational) {
results.simple++
console.log(`${file}${analysis.name} (simple)`)
} else if (analysis.hasHooks || analysis.hasComplexLogic) {
results.needsWrapper++
console.log(` ⚙️ ${file}${analysis.name} (needs wrapper)`)
} else {
results.simple++
console.log(`${file}${analysis.name}`)
}
}
console.log(`\n📊 Results for ${config.category}:`)
console.log(` Total: ${results.total}`)
console.log(` Simple: ${results.simple}`)
console.log(` Wraps UI: ${results.wrapsUI}`)
console.log(` Needs Wrapper: ${results.needsWrapper}`)
console.log(` Failed: ${results.failed}`)
}
/**
* Main conversion process
*/
async function main() {
console.log('🚀 Starting TypeScript to JSON conversion...\n')
const configs: ConversionConfig[] = [
{
sourceDir: 'src/components/atoms',
targetDir: 'src/config/pages/atoms',
category: 'atoms',
},
{
sourceDir: 'src/components/molecules',
targetDir: 'src/config/pages/molecules',
category: 'molecules',
},
{
sourceDir: 'src/components/organisms',
targetDir: 'src/config/pages/organisms',
category: 'organisms',
},
{
sourceDir: 'src/components/ui',
targetDir: 'src/config/pages/ui',
category: 'ui',
},
]
for (const config of configs) {
await convertDirectory(config)
}
console.log('\n✨ Conversion complete!')
console.log('\n📝 Next steps:')
console.log(' 1. Review generated JSON files')
console.log(' 2. Manually fix complex components')
console.log(' 3. Update json-components-registry.json')
console.log(' 4. Test components render correctly')
console.log(' 5. Delete old TypeScript files')
}
main().catch(console.error)

View File

@@ -1,91 +0,0 @@
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, '..')
const missingComponents = [
'AtomicLibraryShowcase',
'CodeEditor',
'ComponentTreeBuilder',
'ComponentTreeManager',
'ConflictResolutionPage',
'DockerBuildDebugger',
'DocumentationView',
'ErrorPanel',
'FaviconDesigner',
'FeatureIdeaCloud',
'FeatureToggleSettings',
'JSONComponentTreeManager',
'JSONLambdaDesigner',
'JSONModelDesigner',
'PersistenceDashboard',
'PersistenceExample',
'ProjectDashboard',
'PWASettings',
'SassStylesShowcase',
'StyleDesigner',
]
async function createComponentJSON(componentName: string) {
// Convert to kebab-case for filename
const fileName = componentName
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, '') + '.json'
const filePath = path.join(rootDir, 'src/config/pages/components', fileName)
// Check if component file exists
const possiblePaths = [
path.join(rootDir, `src/components/${componentName}.tsx`),
path.join(rootDir, `src/components/${componentName}/index.tsx`),
]
let componentPath = ''
for (const p of possiblePaths) {
try {
await fs.access(p)
componentPath = `@/components/${componentName}`
break
} catch {
// Continue searching
}
}
if (!componentPath) {
console.log(` ⚠️ ${componentName} - Component file not found, creating placeholder`)
componentPath = `@/components/${componentName}`
}
const json = {
type: componentName,
jsonCompatible: false,
wrapperRequired: true,
load: {
path: componentPath,
export: componentName,
},
props: {},
}
await fs.writeFile(filePath, JSON.stringify(json, null, 2) + '\n')
console.log(` ✅ Created: ${fileName}`)
}
async function main() {
console.log('📝 Creating JSON definitions for missing custom components...\n')
// Ensure directory exists
const targetDir = path.join(rootDir, 'src/config/pages/components')
await fs.mkdir(targetDir, { recursive: true })
for (const component of missingComponents) {
await createComponentJSON(component)
}
console.log(`\n✨ Created ${missingComponents.length} component JSON files!`)
}
main().catch(console.error)

View File

@@ -1,141 +0,0 @@
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, '..')
// Components we want to remove (restored dependencies)
const targetComponents = {
ui: ['accordion', 'alert', 'aspect-ratio', 'avatar', 'badge', 'button', 'card',
'checkbox', 'collapsible', 'dialog', 'hover-card', 'input', 'label',
'popover', 'progress', 'radio-group', 'resizable', 'scroll-area',
'separator', 'skeleton', 'sheet', 'switch', 'tabs', 'textarea', 'toggle', 'tooltip'],
molecules: ['DataSourceCard', 'EditorToolbar', 'EmptyEditorState', 'MonacoEditorPanel', 'SearchBar'],
organisms: ['EmptyCanvasState', 'PageHeader', 'SchemaEditorCanvas', 'SchemaEditorPropertiesPanel',
'SchemaEditorSidebar', 'SchemaEditorStatusBar', 'SchemaEditorToolbar', 'ToolbarActions'],
atoms: ['Input']
}
interface ImportInfo {
file: string
line: number
importStatement: string
importedComponents: string[]
fromPath: string
}
async function findAllImports(): Promise<ImportInfo[]> {
const imports: ImportInfo[] = []
const searchDirs = [
'src/components',
'src/pages',
'src/lib',
'src'
]
for (const dir of searchDirs) {
const dirPath = path.join(rootDir, dir)
try {
await processDirectory(dirPath, imports)
} catch (e) {
// Directory might not exist, skip
}
}
return imports
}
async function processDirectory(dir: string, imports: ImportInfo[]): Promise<void> {
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory() && !entry.name.includes('node_modules')) {
await processDirectory(fullPath, imports)
} else if (entry.isFile() && (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts'))) {
await processFile(fullPath, imports)
}
}
}
async function processFile(filePath: string, imports: ImportInfo[]): Promise<void> {
const content = await fs.readFile(filePath, 'utf-8')
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// Check for imports from our target components
for (const [category, components] of Object.entries(targetComponents)) {
for (const component of components) {
const patterns = [
`from ['"]@/components/${category}/${component}['"]`,
`from ['"]./${component}['"]`,
`from ['"]../${component}['"]`,
]
for (const pattern of patterns) {
if (new RegExp(pattern).test(line)) {
// Extract imported components
const importMatch = line.match(/import\s+(?:\{([^}]+)\}|(\w+))\s+from/)
const importedComponents = importMatch
? (importMatch[1] || importMatch[2]).split(',').map(s => s.trim())
: []
imports.push({
file: filePath.replace(rootDir, '').replace(/\\/g, '/'),
line: i + 1,
importStatement: line.trim(),
importedComponents,
fromPath: component
})
}
}
}
}
}
}
async function main() {
console.log('🔍 Finding all imports of target components...\n')
const imports = await findAllImports()
if (imports.length === 0) {
console.log('✅ No imports found! Components can be safely deleted.')
return
}
console.log(`❌ Found ${imports.length} imports that need refactoring:\n`)
const byFile: Record<string, ImportInfo[]> = {}
for (const imp of imports) {
if (!byFile[imp.file]) byFile[imp.file] = []
byFile[imp.file].push(imp)
}
for (const [file, fileImports] of Object.entries(byFile)) {
console.log(`📄 ${file}`)
for (const imp of fileImports) {
console.log(` Line ${imp.line}: ${imp.importStatement}`)
console.log(` → Imports: ${imp.importedComponents.join(', ')}`)
}
console.log()
}
console.log('\n📊 Summary by category:')
const byCategory: Record<string, number> = {}
for (const imp of imports) {
const key = imp.fromPath
byCategory[key] = (byCategory[key] || 0) + 1
}
for (const [component, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
console.log(` ${component}: ${count} imports`)
}
}
main().catch(console.error)

View File

@@ -1,127 +0,0 @@
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, '..')
// Components we restored (the ones we want to potentially convert to JSON)
const restoredComponents = {
ui: ['accordion', 'alert', 'aspect-ratio', 'avatar', 'badge', 'button', 'card',
'checkbox', 'collapsible', 'dialog', 'hover-card', 'input', 'label',
'popover', 'progress', 'radio-group', 'resizable', 'scroll-area',
'separator', 'skeleton', 'sheet', 'switch', 'tabs', 'textarea', 'toggle', 'tooltip'],
molecules: ['DataSourceCard', 'EditorToolbar', 'EmptyEditorState', 'MonacoEditorPanel', 'SearchBar'],
organisms: ['EmptyCanvasState', 'PageHeader', 'SchemaEditorCanvas', 'SchemaEditorPropertiesPanel',
'SchemaEditorSidebar', 'SchemaEditorStatusBar', 'SchemaEditorToolbar', 'ToolbarActions'],
atoms: ['Input'],
}
interface ComponentAnalysis {
name: string
category: string
pureJSONEligible: boolean
reasons: string[]
complexity: 'simple' | 'medium' | 'complex'
hasHooks: boolean
hasConditionalLogic: boolean
hasHelperFunctions: boolean
hasComplexProps: boolean
importsCustomComponents: boolean
onlyImportsUIorAtoms: boolean
}
async function analyzeComponent(category: string, component: string): Promise<ComponentAnalysis> {
const tsFile = path.join(rootDir, `src/components/${category}/${component}.tsx`)
const content = await fs.readFile(tsFile, 'utf-8')
const hasHooks = /useState|useEffect|useCallback|useMemo|useReducer|useRef|useContext/.test(content)
const hasConditionalLogic = /\?|if\s*\(|switch\s*\(/.test(content)
const hasHelperFunctions = /(?:const|function)\s+\w+\s*=\s*\([^)]*\)\s*=>/.test(content) && /return\s+\(/.test(content.split('return (')[0] || '')
const hasComplexProps = /\.\w+\s*\?/.test(content) || /Object\./.test(content) || /Array\./.test(content)
// Check imports
const importLines = content.match(/import\s+.*?\s+from\s+['"](.*?)['"]/g) || []
const importsCustomComponents = importLines.some(line =>
/@\/components\/(molecules|organisms)/.test(line)
)
const onlyImportsUIorAtoms = importLines.every(line => {
if (!line.includes('@/components/')) return true
return /@\/components\/(ui|atoms)/.test(line)
})
const reasons: string[] = []
if (hasHooks) reasons.push('Has React hooks')
if (hasHelperFunctions) reasons.push('Has helper functions')
if (hasComplexProps) reasons.push('Has complex prop access')
if (importsCustomComponents) reasons.push('Imports molecules/organisms')
if (!onlyImportsUIorAtoms && !importsCustomComponents) reasons.push('Imports non-UI components')
// Determine if eligible for pure JSON
const pureJSONEligible = !hasHooks && !hasHelperFunctions && !hasComplexProps && onlyImportsUIorAtoms
// Complexity scoring
let complexity: 'simple' | 'medium' | 'complex' = 'simple'
if (hasHooks || hasHelperFunctions || hasComplexProps) {
complexity = 'complex'
} else if (hasConditionalLogic || importsCustomComponents) {
complexity = 'medium'
}
return {
name: component,
category,
pureJSONEligible,
reasons,
complexity,
hasHooks,
hasConditionalLogic,
hasHelperFunctions,
hasComplexProps,
importsCustomComponents,
onlyImportsUIorAtoms,
}
}
async function main() {
console.log('🔍 Analyzing restored components for pure JSON eligibility...\\n')
const eligible: ComponentAnalysis[] = []
const ineligible: ComponentAnalysis[] = []
for (const [category, components] of Object.entries(restoredComponents)) {
for (const component of components) {
try {
const analysis = await analyzeComponent(category, component)
if (analysis.pureJSONEligible) {
eligible.push(analysis)
} else {
ineligible.push(analysis)
}
} catch (e) {
console.log(`⚠️ ${component} - Could not analyze: ${e}`)
}
}
}
console.log(`\\n✅ ELIGIBLE FOR PURE JSON (${eligible.length} components)\\n`)
for (const comp of eligible) {
console.log(` ${comp.name} (${comp.category})`)
console.log(` Complexity: ${comp.complexity}`)
console.log(` Conditional: ${comp.hasConditionalLogic ? 'Yes' : 'No'}`)
}
console.log(`\\n❌ MUST STAY TYPESCRIPT (${ineligible.length} components)\\n`)
for (const comp of ineligible) {
console.log(` ${comp.name} (${comp.category})`)
console.log(` Complexity: ${comp.complexity}`)
console.log(` Reasons: ${comp.reasons.join(', ')}`)
}
console.log(`\\n📊 Summary:`)
console.log(` Eligible for JSON: ${eligible.length}`)
console.log(` Must stay TypeScript: ${ineligible.length}`)
console.log(` Conversion rate: ${Math.round(eligible.length / (eligible.length + ineligible.length) * 100)}%`)
}
main().catch(console.error)

View File

@@ -1,157 +0,0 @@
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, '..')
/**
* Strategy: Replace static imports with dynamic component loading
*
* Before:
* import { Button } from '@/components/ui/button'
* <Button variant="primary">Click</Button>
*
* After:
* import { getComponent } from '@/lib/component-loader'
* const Button = getComponent('Button')
* <Button variant="primary">Click</Button>
*/
interface RefactorTask {
file: string
replacements: Array<{
oldImport: string
newImport: string
components: string[]
}>
}
const targetComponents = {
ui: ['button', 'card', 'badge', 'label', 'input', 'separator', 'scroll-area',
'tabs', 'dialog', 'textarea', 'tooltip', 'switch', 'alert', 'skeleton',
'progress', 'collapsible', 'resizable', 'popover', 'hover-card', 'checkbox',
'accordion', 'aspect-ratio', 'avatar', 'radio-group', 'sheet', 'toggle'],
molecules: ['DataSourceCard', 'EditorToolbar', 'EmptyEditorState', 'MonacoEditorPanel', 'SearchBar'],
organisms: ['EmptyCanvasState', 'PageHeader', 'SchemaEditorCanvas', 'SchemaEditorPropertiesPanel',
'SchemaEditorSidebar', 'SchemaEditorStatusBar', 'SchemaEditorToolbar', 'ToolbarActions'],
atoms: ['Input']
}
export async function refactorFile(filePath: string): Promise<boolean> {
let content = await fs.readFile(filePath, 'utf-8')
let modified = false
// Find all imports to replace
const componentsToLoad = new Set<string>()
for (const [category, components] of Object.entries(targetComponents)) {
for (const component of components) {
const patterns = [
new RegExp(`import\\s+\\{([^}]+)\\}\\s+from\\s+['"]@/components/${category}/${component}['"]`, 'g'),
new RegExp(`import\\s+(\\w+)\\s+from\\s+['"]@/components/${category}/${component}['"]`, 'g'),
]
for (const pattern of patterns) {
const matches = content.matchAll(pattern)
for (const match of matches) {
const importedItems = match[1].split(',').map(s => s.trim().split(' as ')[0].trim())
importedItems.forEach(item => componentsToLoad.add(item))
// Remove the import line
content = content.replace(match[0], '')
modified = true
}
}
}
}
if (!modified) return false
// Add dynamic component loader import at top
const loaderImport = `import { loadComponent } from '@/lib/component-loader'\n`
// Add component loading statements
const componentLoads = Array.from(componentsToLoad)
.map(comp => `const ${comp} = loadComponent('${comp}')`)
.join('\n')
// Find first import statement location
const firstImportMatch = content.match(/^import\s/m)
if (firstImportMatch && firstImportMatch.index !== undefined) {
content = content.slice(0, firstImportMatch.index) +
loaderImport + '\n' +
componentLoads + '\n\n' +
content.slice(firstImportMatch.index)
}
await fs.writeFile(filePath, content)
return true
}
async function createComponentLoader() {
const loaderPath = path.join(rootDir, 'src/lib/component-loader.ts')
const loaderContent = `/**
* Dynamic Component Loader
* Loads components from the registry at runtime instead of static imports
*/
import { ComponentType, lazy } from 'react'
const componentCache = new Map<string, ComponentType<any>>()
export function loadComponent(componentName: string): ComponentType<any> {
if (componentCache.has(componentName)) {
return componentCache.get(componentName)!
}
// Try to load from different sources
const loaders = [
() => import(\`@/components/ui/\${componentName.toLowerCase()}\`),
() => import(\`@/components/atoms/\${componentName}\`),
() => import(\`@/components/molecules/\${componentName}\`),
() => import(\`@/components/organisms/\${componentName}\`),
]
// Create lazy component
const LazyComponent = lazy(async () => {
for (const loader of loaders) {
try {
const module = await loader()
return { default: module[componentName] || module.default }
} catch (e) {
continue
}
}
throw new Error(\`Component \${componentName} not found\`)
})
componentCache.set(componentName, LazyComponent)
return LazyComponent
}
export function getComponent(componentName: string): ComponentType<any> {
return loadComponent(componentName)
}
`
await fs.writeFile(loaderPath, loaderContent)
console.log('✅ Created component-loader.ts')
}
async function main() {
console.log('🚀 Starting AGGRESSIVE refactoring to eliminate static imports...\n')
console.log('⚠️ WARNING: This is a MAJOR refactoring affecting 975+ import statements!\n')
console.log('Press Ctrl+C now if you want to reconsider...\n')
await new Promise(resolve => setTimeout(resolve, 3000))
console.log('🔧 Creating dynamic component loader...')
await createComponentLoader()
console.log('\n📝 This approach requires significant testing and may break things.')
console.log(' Recommendation: Manual refactoring of high-value components instead.\n')
}
main().catch(console.error)

View File

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

View File

@@ -1,262 +0,0 @@
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, '..')
interface JSONComponent {
type: string
jsonCompatible?: boolean
wrapperRequired?: boolean
load?: {
path: string
export: string
lazy?: boolean
}
props?: Record<string, unknown>
metadata?: {
notes?: string
}
}
interface RegistryEntry {
type: string
name: string
category: string
canHaveChildren: boolean
description: string
status: 'supported' | 'deprecated'
source: 'atoms' | 'molecules' | 'organisms' | 'ui' | 'wrappers' | 'custom'
jsonCompatible: boolean
wrapperRequired?: boolean
load?: {
path: string
export: string
lazy?: boolean
}
metadata?: {
conversionDate?: string
autoGenerated?: boolean
notes?: string
}
}
interface Registry {
version: string
categories: Record<string, string>
sourceRoots: Record<string, string[]>
components: RegistryEntry[]
statistics: {
total: number
supported: number
jsonCompatible: number
byCategory: Record<string, number>
bySource: Record<string, number>
}
}
/**
* Determine component category based on name and source
*/
function determineCategory(componentName: string, source: string): string {
const name = componentName.toLowerCase()
// Layout components
if (/container|section|stack|flex|grid|layout|panel|sidebar|header|footer/.test(name)) {
return 'layout'
}
// Input components
if (/input|select|checkbox|radio|slider|switch|form|textarea|date|file|number|password|search/.test(name)) {
return 'input'
}
// Navigation components
if (/nav|menu|breadcrumb|tab|link|pagination/.test(name)) {
return 'navigation'
}
// Feedback components
if (/alert|toast|notification|spinner|loading|progress|skeleton|badge|indicator/.test(name)) {
return 'feedback'
}
// Data display components
if (/table|list|card|chart|graph|tree|timeline|avatar|image/.test(name)) {
return 'data'
}
// Display components
if (/text|heading|label|code|icon|divider|separator|spacer/.test(name)) {
return 'display'
}
// Default to custom for organisms and complex components
if (source === 'organisms' || source === 'molecules') {
return 'custom'
}
return 'display'
}
/**
* Determine if component can have children
*/
function canHaveChildren(componentName: string): boolean {
const name = componentName.toLowerCase()
// These typically don't have children
const noChildren = /input|select|checkbox|radio|slider|switch|image|icon|divider|separator|spacer|spinner|progress|badge|dot/
return !noChildren.test(name)
}
/**
* Generate description for component
*/
function generateDescription(componentName: string, category: string): string {
const descriptions: Record<string, string> = {
layout: 'Layout container component',
input: 'Form input component',
navigation: 'Navigation component',
feedback: 'Feedback and status component',
data: 'Data display component',
display: 'Display component',
custom: 'Custom component',
}
return descriptions[category] || 'Component'
}
/**
* Read all JSON files from a directory and create registry entries
*/
async function processDirectory(
dir: string,
source: 'atoms' | 'molecules' | 'organisms' | 'ui' | 'custom'
): Promise<RegistryEntry[]> {
const entries: RegistryEntry[] = []
try {
const files = await fs.readdir(dir)
const jsonFiles = files.filter(f => f.endsWith('.json'))
for (const file of jsonFiles) {
const filePath = path.join(dir, file)
const content = await fs.readFile(filePath, 'utf-8')
const jsonComponent: JSONComponent = JSON.parse(content)
const componentName = jsonComponent.type
if (!componentName) continue
const category = determineCategory(componentName, source)
const entry: RegistryEntry = {
type: componentName,
name: componentName,
category,
canHaveChildren: canHaveChildren(componentName),
description: generateDescription(componentName, category),
status: 'supported',
source,
jsonCompatible: jsonComponent.jsonCompatible !== false,
wrapperRequired: jsonComponent.wrapperRequired || false,
metadata: {
conversionDate: new Date().toISOString().split('T')[0],
autoGenerated: true,
notes: jsonComponent.metadata?.notes,
},
}
if (jsonComponent.load) {
entry.load = jsonComponent.load
}
entries.push(entry)
}
} catch (error) {
console.error(`Error processing ${dir}:`, error)
}
return entries
}
/**
* Update the registry with new components
*/
async function updateRegistry() {
console.log('📝 Updating json-components-registry.json...\n')
const registryPath = path.join(rootDir, 'json-components-registry.json')
// Read existing registry
const registryContent = await fs.readFile(registryPath, 'utf-8')
const registry: Registry = JSON.parse(registryContent)
console.log(` Current components: ${registry.components.length}`)
// Process each directory
const newEntries: RegistryEntry[] = []
const directories = [
{ dir: path.join(rootDir, 'src/config/pages/atoms'), source: 'atoms' as const },
{ dir: path.join(rootDir, 'src/config/pages/molecules'), source: 'molecules' as const },
{ dir: path.join(rootDir, 'src/config/pages/organisms'), source: 'organisms' as const },
{ dir: path.join(rootDir, 'src/config/pages/ui'), source: 'ui' as const },
{ dir: path.join(rootDir, 'src/config/pages/components'), source: 'custom' as const },
]
for (const { dir, source } of directories) {
const entries = await processDirectory(dir, source)
newEntries.push(...entries)
console.log(` Processed ${source}: ${entries.length} components`)
}
// Merge with existing components (remove duplicates)
const existingTypes = new Set(registry.components.map(c => c.type))
const uniqueNewEntries = newEntries.filter(e => !existingTypes.has(e.type))
console.log(`\n New unique components: ${uniqueNewEntries.length}`)
console.log(` Skipped duplicates: ${newEntries.length - uniqueNewEntries.length}`)
// Add new components
registry.components.push(...uniqueNewEntries)
// Update statistics
const byCategory: Record<string, number> = {}
const bySource: Record<string, number> = {}
for (const component of registry.components) {
byCategory[component.category] = (byCategory[component.category] || 0) + 1
bySource[component.source] = (bySource[component.source] || 0) + 1
}
registry.statistics = {
total: registry.components.length,
supported: registry.components.filter(c => c.status === 'supported').length,
jsonCompatible: registry.components.filter(c => c.jsonCompatible).length,
byCategory,
bySource,
}
// Sort components by type
registry.components.sort((a, b) => a.type.localeCompare(b.type))
// Write updated registry
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n')
console.log(`\n✅ Registry updated successfully!`)
console.log(` Total components: ${registry.statistics.total}`)
console.log(` JSON compatible: ${registry.statistics.jsonCompatible}`)
console.log(`\n📊 By source:`)
for (const [source, count] of Object.entries(bySource)) {
console.log(` ${source.padEnd(12)}: ${count}`)
}
console.log(`\n📊 By category:`)
for (const [category, count] of Object.entries(byCategory)) {
console.log(` ${category.padEnd(12)}: ${count}`)
}
}
updateRegistry().catch(console.error)

View File

@@ -1,153 +1,64 @@
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import { FeatureToggles } from '@/types/project'
import {
BookOpen,
Code,
Cube,
Database,
FileText,
Flask,
FlowArrow,
Image,
Lightbulb,
PaintBrush,
Play,
Tree,
Wrench,
} from '@phosphor-icons/react'
import { ScrollArea } from '@/components/ui/scroll-area'
import featureToggleSettings from '@/config/feature-toggle-settings.json'
import type { ComponentType } from 'react'
import { useMemo } from 'react'
import featureToggleSchema from '@/schemas/feature-toggle-settings.json'
import type { PageSchema } from '@/types/json-ui'
import { evaluateExpression } from '@/lib/json-ui/expression-evaluator'
interface FeatureToggleSettingsProps {
features: FeatureToggles
onFeaturesChange: (features: FeatureToggles) => void
}
type FeatureToggleIconKey =
| 'BookOpen'
| 'Code'
| 'Cube'
| 'Database'
| 'FileText'
| 'Flask'
| 'FlowArrow'
| 'Image'
| 'Lightbulb'
| 'PaintBrush'
| 'Play'
| 'Tree'
| 'Wrench'
const iconMap: Record<FeatureToggleIconKey, ComponentType<{ size?: number; weight?: 'duotone' }>> = {
BookOpen,
Code,
Cube,
Database,
FileText,
Flask,
FlowArrow,
Image,
Lightbulb,
PaintBrush,
Play,
Tree,
Wrench,
}
type FeatureToggleItem = {
key: keyof FeatureToggles
label: string
description: string
icon: FeatureToggleIconKey
}
const featuresList = featureToggleSettings as FeatureToggleItem[]
function FeatureToggleHeader({ enabledCount, totalCount }: { enabledCount: number; totalCount: number }) {
return (
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2">Feature Toggles</h2>
<p className="text-muted-foreground">
Enable or disable features to customize your workspace. {enabledCount} of {totalCount} features enabled.
</p>
</div>
)
}
function FeatureToggleCard({
item,
enabled,
onToggle,
}: {
item: FeatureToggleItem
enabled: boolean
onToggle: (value: boolean) => void
}) {
const Icon = iconMap[item.icon]
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${enabled ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
<Icon size={20} weight="duotone" />
</div>
<div>
<CardTitle className="text-base">{item.label}</CardTitle>
<CardDescription className="text-xs mt-1">{item.description}</CardDescription>
</div>
</div>
<Switch id={item.key} checked={enabled} onCheckedChange={onToggle} />
</div>
</CardHeader>
</Card>
)
}
function FeatureToggleGrid({
items,
features,
onToggle,
}: {
items: FeatureToggleItem[]
features: FeatureToggles
onToggle: (key: keyof FeatureToggles, value: boolean) => void
}) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pr-4">
{items.map((item) => (
<FeatureToggleCard
key={item.key}
item={item}
enabled={features[item.key]}
onToggle={(checked) => onToggle(item.key, checked)}
/>
))}
</div>
)
}
/**
* FeatureToggleSettings - Now JSON-driven!
*
* This component demonstrates how a complex React component with:
* - Custom hooks and state management
* - Dynamic data rendering (looping over features)
* - Event handlers (toggle switches)
* - Conditional styling (enabled/disabled states)
*
* Can be converted to a pure JSON schema with custom action handlers.
* The JSON schema handles all UI structure, data binding, and loops,
* while custom functions handle business logic.
*
* Converted from 153 lines of React/TSX to:
* - 1 JSON schema file (195 lines, but mostly structure)
* - 45 lines of integration code (this file)
*
* Benefits:
* - UI structure is now data-driven and can be modified without code changes
* - Feature list is in JSON and can be easily extended
* - Styling and layout can be customized via JSON
* - Business logic (toggle handler) stays in TypeScript for type safety
*/
export function FeatureToggleSettings({ features, onFeaturesChange }: FeatureToggleSettingsProps) {
const handleToggle = (key: keyof FeatureToggles, value: boolean) => {
onFeaturesChange({
...features,
[key]: value,
})
}
// Custom action handler - this is the "hook" that handles complex logic
const handlers = useMemo(() => ({
updateFeature: (action: any, eventData: any) => {
// Evaluate the params to get the actual values
const context = { data: { features, item: eventData.item }, event: eventData }
// The key param is an expression like "item.key" which needs evaluation
const key = evaluateExpression(action.params.key, context) as keyof FeatureToggles
const checked = eventData as boolean
onFeaturesChange({
...features,
[key]: checked,
})
}
}), [features, onFeaturesChange])
const enabledCount = Object.values(features).filter(Boolean).length
const totalCount = Object.keys(features).length
// Pass features as external data to the JSON renderer
const data = useMemo(() => ({ features }), [features])
return (
<div className="h-full p-6 bg-background">
<FeatureToggleHeader enabledCount={enabledCount} totalCount={totalCount} />
<ScrollArea className="h-[calc(100vh-200px)]">
<FeatureToggleGrid items={featuresList} features={features} onToggle={handleToggle} />
</ScrollArea>
</div>
<PageRenderer
schema={featureToggleSchema as PageSchema}
data={data}
functions={handlers}
/>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,15 @@
export { NavigationMenu } from './NavigationMenu'
export { PageHeader } from './PageHeader'
export { ToolbarActions } from './ToolbarActions'
export { AppHeader } from './AppHeader'
export { DataSourceManager } from './DataSourceManager'
export { TreeListPanel } from './TreeListPanel'
export { SchemaEditorToolbar } from './SchemaEditorToolbar'
export { SchemaEditorSidebar } from './SchemaEditorSidebar'
export { SchemaEditorCanvas } from './SchemaEditorCanvas'
export { SchemaEditorPropertiesPanel } from './SchemaEditorPropertiesPanel'
export { SchemaEditorLayout } from './SchemaEditorLayout'
export { EmptyCanvasState } from './EmptyCanvasState'
export { SchemaEditorStatusBar } from './SchemaEditorStatusBar'
export { SchemaCodeViewer } from './SchemaCodeViewer'
export { JSONUIShowcase } from '../JSONUIShowcase'

View File

@@ -1,15 +0,0 @@
{
"type": "Accordion",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/Accordion",
"export": "Accordion"
},
"props": {
"id": "> {"
},
"metadata": {
"notes": "Contains hooks - needs wrapper"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Button",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Card, CardContent",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "ActionIcon",
"props": {}
}

View File

@@ -1,4 +1,6 @@
{
"type": "Alert",
"props": {}
"props": {
"variant": "default"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "AppLogo",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "AvatarGroup",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Avatar",
"props": {}
}

View File

@@ -1,4 +1,6 @@
{
"type": "Badge as ShadcnBadge",
"props": {}
"type": "Badge",
"props": {
"variant": "default"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Tooltip, TooltipContent, TooltipProvider, TooltipTrigger",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Breadcrumb",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "ButtonGroup",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Button as ShadcnButton, ButtonProps as ShadcnButtonProps",
"props": {}
}

View File

@@ -1,7 +0,0 @@
{
"type": "Calendar as ShadcnCalendar",
"props": {
"onSelect": "> void",
"disabled": "> boolean)"
}
}

View File

@@ -1,6 +1,3 @@
{
"type": "Card",
"props": {
"onClick": "> void"
}
"type": "Card"
}

View File

@@ -1,6 +0,0 @@
{
"type": "Checkbox",
"props": {
"onChange": "> void"
}
}

View File

@@ -1,7 +0,0 @@
{
"type": "Chip",
"props": {
"12": "bold",
"onRemove": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Progress",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Code",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "ColorSwatch",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,7 +0,0 @@
{
"type": "Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,",
"props": {
"onSelect": "> void",
"onOpenChange": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Card, CardContent, CardDescription, CardHeader, CardTitle",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Card",
"props": {
"onDragStart": "> void"
}
}

View File

@@ -1,22 +0,0 @@
{
"type": "ComponentTreeNode",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/ComponentTreeNode",
"export": "ComponentTreeNode"
},
"props": {
"onSelect": "> void",
"onHover": "> void",
"onHoverEnd": "> void",
"onDragStart": "> void",
"onDragOver": "> void",
"onDragLeave": "> void",
"onDrop": "> void",
"onToggleExpand": "> void"
},
"metadata": {
"notes": "Complex logic - needs wrapper"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Button, ButtonProps",
"props": {
"onConfirm": "> void | Promise<void>"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Container",
"props": {}
}

View File

@@ -1,7 +0,0 @@
{
"type": "ContextMenu as ShadcnContextMenu,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuTrigger,\n ContextMenuSeparator,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,",
"props": {
"onSelect": "> void",
"menuItems": "> {"
}
}

View File

@@ -1,13 +0,0 @@
{
"type": "CopyButton",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/CopyButton",
"export": "CopyButton"
},
"props": {},
"metadata": {
"notes": "Contains hooks - needs wrapper"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Badge",
"props": {}
}

View File

@@ -1,7 +0,0 @@
{
"type": "DataList",
"props": {
"renderItem": "> ReactNode",
"item": "> {"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Badge",
"props": {}
}

View File

@@ -1,7 +0,0 @@
{
"type": "Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,",
"props": {
"cell": "> ReactNode",
"onRowClick": "> void"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Popover, PopoverContent, PopoverTrigger",
"props": {
"onChange": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Card, CardContent",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Divider",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Dot",
"props": {}
}

View File

@@ -1,9 +0,0 @@
{
"type": "Drawer",
"props": {
"onClose": "> void",
"sm": "== ",
"md": "== ",
"lg": "== "
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Button",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "EmptyStateIcon",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Button",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Badge",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "FileIcon",
"props": {}
}

View File

@@ -1,18 +0,0 @@
{
"type": "FileUpload",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/FileUpload",
"export": "FileUpload"
},
"props": {
"onFilesSelected": "> void",
"files": "> {",
"e": "> {",
"index": "> {"
},
"metadata": {
"notes": "Contains hooks - needs wrapper"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Input",
"props": {
"onChange": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Flex",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Form as ShadcnForm,\n FormControl,\n FormDescription,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,",
"props": {
"onSubmit": "> void | Promise<void>"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Card",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Grid",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Heading",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "HelperText",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "HoverCard as ShadcnHoverCard,\n HoverCardContent,\n HoverCardTrigger,",
"props": {}
}

View File

@@ -1,6 +0,0 @@
{
"type": "Button",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "IconText",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "IconWrapper",
"props": {}
}

View File

@@ -1,18 +0,0 @@
{
"type": "Image",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/Image",
"export": "Image"
},
"props": {
"onLoad": "> void",
"onError": "> void",
"width": "== ",
"height": "== "
},
"metadata": {
"notes": "Contains hooks - needs wrapper"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "InfoBox",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "InfoPanel",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Input",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Kbd",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "KeyValue",
"props": {}
}

View File

@@ -1,4 +1,3 @@
{
"type": "Label",
"props": {}
"type": "Label"
}

View File

@@ -1,6 +0,0 @@
{
"type": "Link",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "ListItem",
"props": {
"onClick": "> void"
}
}

View File

@@ -1,6 +0,0 @@
{
"type": "List",
"props": {
"renderItem": "> ReactNode"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "LiveIndicator",
"props": {}
}

View File

@@ -1,4 +1,7 @@
{
"id": "loading-spinner",
"type": "LoadingSpinner",
"props": {}
"props": {
"size": "md"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "LoadingState",
"props": {}
}

View File

@@ -1,17 +0,0 @@
{
"type": "Menu",
"jsonCompatible": false,
"wrapperRequired": true,
"load": {
"path": "@/components/atoms/Menu",
"export": "Menu"
},
"props": {
"onClick": "> void",
"event": "> {",
"item": "> {"
},
"metadata": {
"notes": "Contains hooks - needs wrapper"
}
}

View File

@@ -1,4 +0,0 @@
{
"type": "Card, CardContent",
"props": {}
}

View File

@@ -1,4 +0,0 @@
{
"type": "MetricDisplay",
"props": {}
}

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