Compare commits

...

206 Commits

Author SHA1 Message Date
56376b1094 Merge pull request #208 from johndoe6345789/silly-darwin
feat: Add JSON definitions for menu, password input, and popover comp…
2026-01-19 01:31:51 +00:00
809803283b feat: Add JSON definitions for menu, password input, and popover components
- Introduced `menu.json` for menu component structure with bindings for trigger and content.
- Created `password-input.json` for password input handling visibility and value changes.
- Added `popover.json` for popover component with trigger and content bindings.

feat: Implement custom hooks for UI interactions

- Added `useAccordion` for managing accordion state with single/multiple item support.
- Created `useBindingEditor` for managing bindings in a dynamic editor.
- Implemented `useCopyState` for clipboard copy functionality with feedback.
- Developed `useFileUpload` for handling file uploads with drag-and-drop support.
- Introduced `useFocusState` for managing focus state in components.
- Created `useImageState` for handling image loading and error states.
- Added `useMenuState` for managing menu interactions and item clicks.
- Implemented `usePasswordVisibility` for toggling password visibility.
- Developed `usePopoverState` for managing popover visibility and interactions.

feat: Add constants and interfaces for JSON UI components

- Introduced constants for sizes, placements, styles, and object-fit handling.
- Created interfaces for various components including Accordion, Binding Editor, Copy Button, Data Source Editor, File Upload, and more.
- Added type definitions for menu items, popover props, and other UI elements to enhance type safety and maintainability.
2026-01-19 01:30:42 +00:00
f0c5680b44 Merge pull request #207 from johndoe6345789/copilot/sub-pr-206
Implement pure JSON component system with hooks loader, fix import paths, delete all wrapper components, and fix build errors
2026-01-19 00:21:43 +00:00
copilot-swe-agent[bot]
5a70926d9d fix: Resolve all unrelated build errors
- Added BasicPageHeader export to atoms/index.ts (was only exported as PageHeader)
- Added Accordion export to atoms/index.ts (file existed but wasn't exported)
- Created AppBranding component in molecules/ (was missing)
- Added AppBranding export to molecules/index.ts
- Added missing exports to FeatureIdeaCloud/constants.ts (GROUP_COLORS, CATEGORIES, PRIORITIES, STATUSES)
- Build now completely clean with zero errors

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:58:27 +00:00
copilot-swe-agent[bot]
fc231f727d fix: Correct className transform in JSON definitions
- Fixed lazy-d3-bar-chart.json className binding to use className instead of data
- Fixed component-tree.json className binding to use className instead of data
- Code review feedback addressed

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:51:49 +00:00
copilot-swe-agent[bot]
4ac334a12c feat: Convert final 2 components - ALL wrappers now pure JSON! 🎉
- Created lazy-d3-bar-chart.json with SVG rendering using chartData hook
- Created storage-settings.json with backend switching and import/export
- Deleted LazyD3BarChartWrapper.tsx and StorageSettingsWrapper.tsx
- Updated exports to use JSON components
- ALL 11 components now pure JSON - zero wrappers remaining
- Complete JSON component system with hooks loader proven working

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:51:09 +00:00
copilot-swe-agent[bot]
e549f4e0de feat: Convert ComponentTree and SeedDataManager to pure JSON with hooks
- Created useComponentTree hook to flatten recursive tree structure
- Created useStorageBackendInfo hook for backend icon/copy logic
- Created useD3BarChart hook for chart calculations
- Registered all new hooks in hooks-registry
- Created component-tree.json with list rendering using treeData from hook
- Created seed-data-manager.json with full Card/Alert/Button structure
- Deleted ComponentTreeWrapper.tsx and SeedDataManagerWrapper.tsx
- Updated exports to use JSON components
- 9 components now pure JSON, 2 wrappers remaining

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:48:23 +00:00
copilot-swe-agent[bot]
db8759f9e0 feat: Delete ComponentBindingDialog and DataSourceEditorDialog wrappers, update exports
- Deleted ComponentBindingDialogWrapper.tsx and DataSourceEditorDialogWrapper.tsx
- These already have JSON definitions and were exported from json-components
- Updated molecules/index.ts to export from json-components instead of wrappers
- Reduced remaining wrappers from 6 to 4
- All deleted wrappers now pure JSON components

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:29:36 +00:00
copilot-swe-agent[bot]
4c17cc49c1 feat: Implement hooks loader system for JSON components, convert SaveIndicator to pure JSON
- Created hooks-registry.ts for registering custom React hooks
- Created createJsonComponentWithHooks for JSON components that need hooks
- Implemented SaveIndicator as pure JSON with useSaveIndicator hook
- Moved JSON definitions from wrappers/definitions to components/json-definitions
- Removed wrappers folder entirely
- Fixed NavigationItem JSON to include onClick handler binding
- Deleted legacy NavigationItem.tsx and PageHeaderContent.tsx files
- Architecture: JSON + interfaces + hook loader = fully functional components

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:22:40 +00:00
copilot-swe-agent[bot]
0514e61000 feat: Remove wrapper system, implement pure JSON components
- Created src/lib/json-ui/interfaces/ folder for component interfaces
- Created createJsonComponent() helper for pure JSON component creation
- Deleted wrapper TypeScript files (LoadingFallbackWrapper, NavigationItemWrapper, PageHeaderContentWrapper, TreeCardWrapper)
- Components now export directly from JSON definitions via json-components.ts
- No more wrapper layer - components are pure JSON with separate interface definitions
- Architecture simplified: JSON definition + interface = component

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 23:13:00 +00:00
copilot-swe-agent[bot]
ad68fe8f44 feat: Delete old rigid TypeScript components now loaded via JSON registry
- Deleted TreeCard.tsx and TreeListHeader.tsx
- These components are now loaded dynamically through JSON registry
- import.meta.glob automatically discovers them without TypeScript files
- Consumers already refactored to not import from these files
- JSON system working correctly - no more rigid TypeScript components needed

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 22:36:23 +00:00
copilot-swe-agent[bot]
c2112afe29 fix: Update AtomicComponentDemo to import from correct locations, bypass index.ts
- Import DataCard from @/components/atoms/json-ui instead of molecules
- Replace ActionBar with inline Button components
- This bypasses the molecules/index.ts exports as intended
- Components now loaded directly or through JSON registry

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 22:31:07 +00:00
copilot-swe-agent[bot]
9a6c7f8652 fix: Remove duplicate LoadingFallbackWrapper export
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 22:19:57 +00:00
copilot-swe-agent[bot]
a65a994ec4 feat: Implement JSON components with wrappers for LoadingFallback, NavigationItem, PageHeaderContent
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 22:07:46 +00:00
copilot-swe-agent[bot]
a386551f23 Fix: Inline NavigationGroupHeader to complete component migration
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 22:01:07 +00:00
copilot-swe-agent[bot]
1bb2eadbe8 Fix: Inline deleted JSON components to resolve build errors
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 21:58:04 +00:00
copilot-swe-agent[bot]
56aeb492e6 Initial plan: Root cause analysis of JSON component system
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-18 21:52:57 +00:00
b3fa462527 Update scripts/refactor-to-dynamic-imports.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 21:47:20 +00:00
2478948bcb Update e2e/debug.spec.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 21:47:13 +00:00
ff37033102 Update e2e/debug.spec.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 21:47:03 +00:00
e63d32eb05 Update scripts/analyze-pure-json-candidates.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 21:46:55 +00:00
5f92fbbf54 Update scripts/refactor-to-dynamic-imports.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 21:46:46 +00:00
copilot-swe-agent[bot]
d9f2688a91 Initial plan 2026-01-18 21:46:38 +00:00
bef28e8c91 feat: add Timestamp and Toggle components; implement TreeIcon and EditorActions components
- Introduced Timestamp component for displaying formatted dates and relative time.
- Added Toggle component for switch-like functionality with customizable sizes.
- Implemented TreeIcon component for rendering tree icons using Phosphor icons.
- Created EditorActions component for explain and improve actions with icons.
- Developed FileTabs component for managing open files with close functionality.
- Added LazyInlineMonacoEditor and LazyMonacoEditor for lazy loading Monaco editor.
- Implemented NavigationItem for navigation with badges and icons.
- Created PageHeaderContent for displaying page headers with icons and descriptions.
- Added JSON configuration files for various UI components and layouts.
- Enhanced data binding with new computed data source hook.
- Updated component registry and types for new components.
- Configured Vite for improved hot module replacement experience.
2026-01-18 21:42:51 +00:00
f69220e7e4 Restore 52 component files that were dependencies
Issue: When we deleted 123 "simple" components, we inadvertently deleted components that were dependencies for other parts of the system, causing build errors.

Solution: Restored all deleted components that are imported by other files.

Restored files:
UI Components (24): 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 (5): DataSourceCard, EditorToolbar, EmptyEditorState, MonacoEditorPanel, SearchBar

Organisms (8): EmptyCanvasState, PageHeader, SchemaEditorCanvas, SchemaEditorPropertiesPanel, SchemaEditorSidebar, SchemaEditorStatusBar, SchemaEditorToolbar, ToolbarActions

Atoms (1): Input

Total restored: 38 files

Remaining Issue:
- Pre-existing bug in component-registry.ts: import.meta.glob cannot use dynamic patterns from JSON
- This requires a refactor to use static glob patterns
- Not introduced by our changes - was already in codebase

Current Status:
- Most import errors resolved
- Application can compile with restored dependencies
- JSON component definitions remain intact and functional

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 20:08:55 +00:00
043eb427d3 Fix all validation errors - add missing component definitions
Fixed all "Unknown component type" validation errors by:

1. Restored Card sub-components (CardHeader, CardTitle, CardDescription, CardContent, CardFooter)
   - Restored src/components/ui/card.tsx with all sub-components
   - Created JSON definitions for all 5 Card sub-components

2. Restored Tabs sub-components (TabsList, TabsTrigger, TabsContent)
   - Restored src/components/ui/tabs.tsx with all sub-components
   - Created JSON definitions for all 3 Tabs sub-components

3. Added 20 missing custom page components
   - Created JSON wrappers for all custom page components
   - AtomicLibraryShowcase, CodeEditor, ComponentTreeBuilder, etc.
   - All components properly reference their TypeScript implementations

4. Updated registry system
   - Enhanced update-registry-from-json.ts to support 'custom' source
   - Added components directory scanning
   - Registry now includes all 342 components

Results:
-  All "Unknown component type" errors resolved
-  Registry updated: 322 → 342 components (+20)
-  28 new JSON files created (8 UI sub-components + 20 custom components)
- ⚠️ Remaining validation errors are pre-existing schema format issues in page definitions (unrelated to component conversion)

Registry Statistics:
- Total components: 342
- JSON compatible: 119
- By source: atoms (142), ui (65), molecules (45), icons (38), custom (20), organisms (16), wrappers (10), primitive (6)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 19:59:10 +00:00
3864fd247a Fix syntax errors preventing dev server startup
Fixed two critical syntax errors:
1. renderer.tsx:363 - Extra closing brace in form field rendering
2. syncSlice.ts:81-86 - Missing closing brace and duplicate code in sync loop

Changes:
- Fixed renderer.tsx form field map closing
- Cleaned up syncSlice.ts for loop structure
- Dev server now starts successfully on port 5001

These were pre-existing errors unrelated to JSON component conversion.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 19:51:34 +00:00
aa51074380 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>
2026-01-18 19:45:25 +00:00
cf74c35e0a Add JSON component definitions for all 375 components
- Created automated conversion script (convert-tsx-to-json.ts)
- Generated 234 JSON component definitions across atoms, molecules, organisms, UI
- Updated json-components-registry.json with 72 new components (317 total)
- Registry now tracks: 142 atoms, 45 molecules, 16 organisms, 60 UI components

Conversion breakdown:
- 124 simple presentational components (ready for TypeScript deletion)
- 61 components wrapping UI libraries (TypeScript kept)
- 19 components needing wrappers (TypeScript kept for hook logic)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 19:37:00 +00:00
f181bb870a Merge pull request #201 from johndoe6345789/codex/add-json-registry-validation-script
Add JSON component registry export validation
2026-01-18 18:49:57 +00:00
05d9034366 Merge pull request #204 from johndoe6345789/codex/add-tests-for-json-components-registry
Add json-ui registry coverage test
2026-01-18 18:49:37 +00:00
29d59ec863 Add registry coverage test for json ui 2026-01-18 18:49:23 +00:00
8841b74027 Merge pull request #203 from johndoe6345789/codex/update-component-registry-paths-and-sources
Prefer explicit `load.path` for JSON components and resolve explicit module paths first
2026-01-18 18:48:59 +00:00
d351f05b14 Merge branch 'main' into codex/update-component-registry-paths-and-sources 2026-01-18 18:48:07 +00:00
85fb859131 Add explicit component paths to JSON registry 2026-01-18 18:47:07 +00:00
d408ceff79 Merge pull request #202 from johndoe6345789/codex/add-build-step-for-typescript-union-generation
Generate JSON UI component types from registry and wire into dev/build
2026-01-18 18:46:36 +00:00
b8dc6f38e6 Generate JSON UI component types 2026-01-18 18:46:02 +00:00
73959e3d48 Add registry export validation script 2026-01-18 18:45:39 +00:00
d20609ecbd Merge pull request #200 from johndoe6345789/codex/add-registry-field-for-source-mapping
Add `sourceRoots` config and build module maps from registry globs
2026-01-18 18:45:13 +00:00
4cb9c01748 Add sourceRoots config for component registry 2026-01-18 18:44:59 +00:00
862e676296 Merge pull request #199 from johndoe6345789/codex/extend-json-config-for-dynamic-loading
Load showcase configs dynamically and add per-example metadata
2026-01-18 18:44:25 +00:00
32dd4d0eac Update showcase config metadata and loading 2026-01-18 18:44:07 +00:00
b34e45067d Merge pull request #198 from johndoe6345789/codex/add-unit-tests-for-autosyncmanager-ijr1r0
Export AutoSyncManager and add unit tests
2026-01-18 18:40:05 +00:00
1a928a29dc Add AutoSyncManager unit tests 2026-01-18 18:38:22 +00:00
27dfebcb24 Merge pull request #197 from johndoe6345789/codex/refactor-computed-json-configs
Remove legacy `computed` data sources; migrate to expression/valueTemplate and update UI
2026-01-18 18:33:08 +00:00
03cc955d20 Update data source editor copy 2026-01-18 18:32:55 +00:00
8c11895fba Merge pull request #195 from johndoe6345789/codex/expand-tests-for-syncfromflaskbulk
Make syncFromFlaskBulk ignore invalid keys, merge remote data and prune local DB; add tests
2026-01-18 18:30:37 +00:00
82b64785bf Merge branch 'main' into codex/expand-tests-for-syncfromflaskbulk 2026-01-18 18:30:23 +00:00
aea8676a33 Add syncFromFlaskBulk merge tests 2026-01-18 18:29:08 +00:00
6abf9f8414 Merge pull request #194 from johndoe6345789/codex/extract-shared-action-names-to-module
Centralize store action name lookups and use Sets in middleware
2026-01-18 18:28:42 +00:00
ee7bc50881 Centralize store action name lookups 2026-01-18 18:28:20 +00:00
f186d67d20 Merge pull request #193 from johndoe6345789/codex/define-syncable_stores-and-refactor-conditions
Centralize syncable stores in syncSlice with `SYNCABLE_STORES`
2026-01-18 18:27:53 +00:00
0c375283ed Merge branch 'main' into codex/define-syncable_stores-and-refactor-conditions 2026-01-18 18:27:46 +00:00
7544c5c2e5 Define syncable store set 2026-01-18 18:27:12 +00:00
5d95bc428b Merge pull request #192 from johndoe6345789/codex/extend-json-components-registry-for-wrappers/icons
Move wrapper/icon resolution into JSON registry
2026-01-18 18:26:50 +00:00
fdd1828fda Move wrapper/icon resolution into JSON registry 2026-01-18 18:26:39 +00:00
c3a05607ba Merge pull request #191 from johndoe6345789/codex/add-tests-for-ratelimiter.throttle
RateLimiter: add bounded retry loop and throttle tests
2026-01-18 18:25:24 +00:00
6c777ed47c Merge branch 'main' into codex/add-tests-for-ratelimiter.throttle 2026-01-18 18:24:45 +00:00
ce9fcaf3d1 Add rate limiter throttle tests 2026-01-18 18:23:56 +00:00
bda28a71e4 Merge pull request #190 from johndoe6345789/codex/add-tests-for-persistencequeue-enqueuing
Ensure PersistenceQueue re-flushes operations enqueued mid-flight and add unit test
2026-01-18 18:23:36 +00:00
4eb4849d57 Merge branch 'main' into codex/add-tests-for-persistencequeue-enqueuing 2026-01-18 18:23:29 +00:00
e098b9184b Add PersistenceQueue mid-flight flush test 2026-01-18 18:22:34 +00:00
b931164c3a Merge pull request #189 from johndoe6345789/codex/modify-syncmonitormiddleware-to-check-requestid
Guard sync monitor tracking by requestId
2026-01-18 18:22:16 +00:00
7d75c6adc0 Guard sync monitor tracking by requestId 2026-01-18 18:22:00 +00:00
33e49b3671 Merge pull request #188 from johndoe6345789/codex/add-runtime-validation-for-json-responses
Validate AI JSON responses with Zod and show actionable toasts on failure
2026-01-18 18:21:40 +00:00
ace40f7e73 Add runtime validation for AI responses 2026-01-18 18:21:25 +00:00
140fe351f8 Merge pull request #187 from johndoe6345789/codex/validate-keys-and-add-reconciliation-step
Validate sync keys and reconcile local data
2026-01-18 18:21:06 +00:00
714fb510ab Validate sync keys and reconcile local data 2026-01-18 18:20:53 +00:00
9c3cc81c35 Merge pull request #186 from johndoe6345789/codex/rethrow-errors-in-synctoflask
Rethrow Flask sync errors and add retryable persistence syncs
2026-01-18 18:20:32 +00:00
def3259178 Handle Flask sync failures with retries 2026-01-18 18:20:22 +00:00
51040a23b9 Merge pull request #182 from johndoe6345789/codex/refactor-ratelimiter.throttle-for-retries
Refactor rate limiter retry loop
2026-01-18 18:19:35 +00:00
785d6afc40 Merge pull request #185 from johndoe6345789/codex/update-persistencequeue-for-proper-flushing
Fix persistence queue flush ordering
2026-01-18 18:19:11 +00:00
0a0046c2f3 Fix persistence queue flush ordering 2026-01-18 18:18:58 +00:00
a0d65352a9 Merge pull request #183 from johndoe6345789/codex/update-dynamic-component-resolution-in-registry
Refactor JSON UI component registry to dynamic resolution
2026-01-18 18:15:58 +00:00
baf5001704 Refactor JSON UI component registry 2026-01-18 18:15:46 +00:00
e075908a15 Refactor rate limiter retry loop 2026-01-18 18:15:25 +00:00
20f116d623 Merge pull request #181 from johndoe6345789/codex/add-inflight-flag-to-autosyncmanager
Add in-flight/pending guards to AutoSyncManager to avoid concurrent syncs
2026-01-18 18:15:05 +00:00
eb9174c80d Add in-flight guard to auto sync 2026-01-18 18:14:51 +00:00
cd9e65d4d2 Merge pull request #180 from johndoe6345789/codex/update-flaskbackendadapter-for-empty-responses
Guard JSON parsing for 204/empty responses in FlaskBackendAdapter
2026-01-18 18:07:25 +00:00
b646b8993f Merge branch 'main' into codex/update-flaskbackendadapter-for-empty-responses 2026-01-18 18:06:35 +00:00
f07bd37b7d Handle empty Flask responses 2026-01-18 18:05:45 +00:00
9f6b0bd871 Merge pull request #177 from johndoe6345789/codex/add-null-check-for-navigation-timing
Add navigation fallback for bundle performance analysis
2026-01-18 18:03:21 +00:00
1b3e0fecfe Add navigation entry fallback in metrics 2026-01-18 18:03:11 +00:00
425797d5a2 Merge pull request #176 from johndoe6345789/codex/refactor-message-handler-in-use-pwa.ts
Refactor PWA service worker message handler to enable cleanup
2026-01-18 18:02:48 +00:00
9dd5a9c6d0 refactor pwa message handler 2026-01-18 18:02:34 +00:00
adb762d0cd Merge pull request #175 from johndoe6345789/codex/add-unit-tests-for-detectandinitialize
Prefer Flask over IndexedDB in UnifiedStorage and add unit tests
2026-01-18 18:02:05 +00:00
e3e3dbf602 Add tests for unified storage initialization 2026-01-18 18:01:54 +00:00
eb8f249946 Merge pull request #174 from johndoe6345789/codex/add-test-for-flaskbackendadapter.request
Handle empty/204 Flask backend responses and add Vitest for delete/clear
2026-01-18 18:01:36 +00:00
ea21a99c8c Add test for empty Flask backend responses 2026-01-18 18:00:47 +00:00
90d392356d Merge pull request #173 from johndoe6345789/codex/create-tests-for-generateflaskblueprint
Sanitize Flask blueprint and endpoint names; add tests
2026-01-18 17:59:35 +00:00
3d7c59557d Merge branch 'main' into codex/create-tests-for-generateflaskblueprint 2026-01-18 17:59:29 +00:00
818f9878aa Add identifier-safe Flask blueprint tests 2026-01-18 17:58:47 +00:00
c618870d4b Merge pull request #172 from johndoe6345789/codex/add-helper-to-normalize-identifiers
Add sanitizeIdentifier helper and use in Flask generators
2026-01-18 17:58:27 +00:00
2641793e0f Add identifier sanitization for Flask generators 2026-01-18 17:58:01 +00:00
fb971be80b Merge pull request #171 from johndoe6345789/codex/extend-json-schema-for-component-registry
Make json-ui registry sources data-driven
2026-01-18 17:57:31 +00:00
395ab80610 Make json-ui registry sources data-driven 2026-01-18 17:57:15 +00:00
91969e8494 Merge pull request #169 from johndoe6345789/codex/remove-static-componentregistry-map
Use shared component registry for orchestration
2026-01-18 17:49:17 +00:00
a6a44f026f Use shared component registry in orchestration 2026-01-18 17:49:07 +00:00
52f1703c35 Merge pull request #168 from johndoe6345789/codex/update-schemas-to-remove-legacy-compute-functions
Replace legacy compute usage with JSON expressions
2026-01-18 17:42:12 +00:00
966b6f2aa6 Remove legacy compute schema support 2026-01-18 17:41:57 +00:00
9a8e159177 Merge pull request #167 from johndoe6345789/codex/define-json-page-prop-contract
Add JSON page data/functions prop mapping
2026-01-18 17:29:35 +00:00
174f03edd2 Add JSON page data/function prop mapping 2026-01-18 17:29:18 +00:00
42c22aa1ea Merge pull request #166 from johndoe6345789/codex/update-pageconfig-to-support-json-pages
Support JSON pages in page config (discriminator + schemaPath)
2026-01-18 17:25:40 +00:00
f6673e1b77 Add JSON page support to page configs 2026-01-18 17:25:22 +00:00
a718aca6f5 Merge pull request #161 from johndoe6345789/codex/add-json-page-rendering-in-routes
Add JSON page routing support
2026-01-18 17:15:51 +00:00
7be52ffc1e Add JSON page routing support 2026-01-18 17:15:39 +00:00
6388880362 Merge pull request #160 from johndoe6345789/codex/audit-and-refactor-json/ui-eval-usage
Replace eval-based bindings with JSON evaluator helpers
2026-01-18 17:09:16 +00:00
c6208fafd1 Replace eval bindings with JSON expression evaluator 2026-01-18 17:09:02 +00:00
352ceba09f Merge pull request #159 from johndoe6345789/codex/remove-old-json-system-cruft
Remove legacy JSON docs and component report
2026-01-18 16:52:59 +00:00
777a4b8277 Remove legacy JSON docs and report 2026-01-18 16:52:34 +00:00
1b01492891 Merge pull request #158 from johndoe6345789/codex/update-legacy-action-types-in-json-configs
Migrate JSON pages to json-ui action/conditional syntax and improve event/expression handling
2026-01-18 16:19:55 +00:00
bbcc91dc80 Update JSON UI actions and conditionals 2026-01-18 16:19:42 +00:00
375d3286e8 Merge pull request #157 from johndoe6345789/codex/migrate-to-new-json-ui-components
Migrate JSONLambdaDesigner to json-ui PageRenderer and remove legacy schema renderer
2026-01-18 16:08:43 +00:00
83864189a5 Replace legacy schema renderer usage 2026-01-18 16:08:03 +00:00
f547d38539 things 2026-01-18 15:48:29 +00:00
9a9d76865b Merge pull request #156 from johndoe6345789/codex/extend-delete-action-logic-in-use-action-executor
Support computed and nested selectors for `delete` actions
2026-01-18 13:37:02 +00:00
39e5385925 Extend delete action selector resolution 2026-01-18 13:36:49 +00:00
fa3b31c896 Merge pull request #155 from johndoe6345789/codex/add-open-dialog-and-close-dialog-cases
Support open-dialog/close-dialog actions and uiState.dialogs convention
2026-01-18 13:36:27 +00:00
0475085300 Merge branch 'main' into codex/add-open-dialog-and-close-dialog-cases 2026-01-18 13:36:18 +00:00
5080026ef7 Add dialog action handling 2026-01-18 13:35:43 +00:00
d2cc3d60a0 Merge pull request #154 from johndoe6345789/codex/update-json-ui-context-and-path-handling
Support dotted action targets for nested data updates
2026-01-18 13:35:13 +00:00
67f2c26f10 Add nested target path updates 2026-01-18 13:35:00 +00:00
9ea7c15f5d Merge pull request #153 from johndoe6345789/codex/unify-breadcrumb-components-implementation
Mark Breadcrumb as JSON compatible
2026-01-18 13:17:10 +00:00
92e9b02d6d Mark breadcrumb as JSON compatible 2026-01-18 13:16:08 +00:00
6f01619141 Merge pull request #152 from johndoe6345789/codex/add-component-types-and-register-them
Support branding and navigation molecules in JSON UI
2026-01-18 13:15:22 +00:00
f627f6955f Support branding and navigation molecules 2026-01-18 13:15:04 +00:00
3f190f7e5a Merge pull request #151 from johndoe6345789/codex/add-component-types-and-registration
Add ListItem and table subcomponents + List/Table/Timeline bindings example
2026-01-18 13:14:43 +00:00
7d04abb7d9 Add list/table/timeline bindings example 2026-01-18 13:14:31 +00:00
c439bd733e Merge pull request #150 from johndoe6345789/codex/update-json-ui-components-and-registry-u03e4j
Add DatePicker and FileUpload to JSON UI types, registry and defaults
2026-01-18 13:13:54 +00:00
9fb7765c51 Merge branch 'main' into codex/update-json-ui-components-and-registry-u03e4j 2026-01-18 13:13:48 +00:00
968efc7701 Add DatePicker and FileUpload defaults 2026-01-18 13:12:25 +00:00
d05d16b827 Merge pull request #148 from johndoe6345789/codex/create-json-showcase-page-for-batches
Add JSON Conversion Showcase page/component and remove visual regression snapshot
2026-01-18 13:12:19 +00:00
31d6334a65 Remove visual regression snapshot 2026-01-18 13:11:20 +00:00
f8b9ce6114 Merge pull request #147 from johndoe6345789/codex/update-json-showcase-schema-examples
Enrich new-molecules JSON showcase with realistic data sources & bindings
2026-01-18 13:06:39 +00:00
156e471f0b Update new molecules JSON showcase examples 2026-01-18 13:06:22 +00:00
edbb2f4af0 Merge pull request #140 from johndoe6345789/codex/identify-components-for-hooks-and-context
Add JSON wrappers for hook-dependent components and register them in the registry
2026-01-18 13:05:45 +00:00
83b5e51b7e Merge branch 'main' into codex/identify-components-for-hooks-and-context 2026-01-18 13:05:37 +00:00
2a4b527485 Merge pull request #146 from johndoe6345789/codex/add-circularprogress-and-progressbar-components
Register CircularProgress/Divider/ProgressBar in JSON UI and add demo schema
2026-01-18 13:03:48 +00:00
0a491528f3 Add progress components to JSON UI schemas 2026-01-18 13:03:37 +00:00
3c96b733b2 Move wrapper layouts into JSON definition files 2026-01-18 13:03:24 +00:00
21ef3d1d3e Render JSON wrappers from schema definitions 2026-01-18 13:03:14 +00:00
6df9c0c3dd Resolve wrapper components via registry metadata 2026-01-18 13:03:05 +00:00
5f921e6193 Merge pull request #145 from johndoe6345789/codex/define-deprecation-process-for-components
Add deprecation workflow and runtime warnings for deprecated JSON components
2026-01-18 12:49:14 +00:00
571fe3ef2c Add deprecation guidance and schema warnings 2026-01-18 12:49:03 +00:00
3a89430b29 Merge pull request #144 from johndoe6345789/codex/create-json-fixture-schemas-for-testing
Add dev/QA smoke fixture schemas
2026-01-18 12:48:00 +00:00
37442350cd Add dev QA smoke fixture schemas 2026-01-18 12:47:43 +00:00
bd6bd97894 Merge pull request #143 from johndoe6345789/codex/validate-json-schemas-at-build-time
Add build-time JSON schema validation for UI components
2026-01-18 12:41:18 +00:00
dc0cb8d873 Merge branch 'main' into codex/validate-json-schemas-at-build-time 2026-01-18 12:41:10 +00:00
d725045671 Add schema validation for JSON UI 2026-01-18 12:40:51 +00:00
2375630d37 Merge pull request #142 from johndoe6345789/codex/define-lint-rules-for-props-and-bindings
Add JSON UI schema linting and integrate into lint scripts
2026-01-18 12:40:27 +00:00
ec78ec0f9b Add JSON UI schema linting 2026-01-18 12:40:16 +00:00
a00a9c4b1d Merge pull request #141 from johndoe6345789/codex/create-components-compatibility-table
docs: update JSON compatibility table to reflect refactoring model
2026-01-18 12:39:56 +00:00
79732ce358 docs: update JSON compatibility table 2026-01-18 12:39:46 +00:00
c345e892f9 Add JSON wrapper components for hook-based UI 2026-01-18 12:38:39 +00:00
a7ce7b0be6 Merge pull request #139 from johndoe6345789/codex/add-component-types-and-update-registry
Register converted JSON UI components, extend types and add default definitions
2026-01-18 12:37:14 +00:00
10a7719e49 Merge branch 'main' into codex/add-component-types-and-update-registry 2026-01-18 12:37:05 +00:00
578b52bb95 Add converted JSON UI components 2026-01-18 12:36:11 +00:00
a283626538 Merge pull request #138 from johndoe6345789/codex/identify-and-prioritize-high-impact-components
Add component usage report
2026-01-18 12:33:24 +00:00
6cc5adf870 Merge pull request #137 from johndoe6345789/codex/define-naming-rules-and-document-in-json
docs: define JSON component naming conventions
2026-01-18 12:32:48 +00:00
6d4775fb5a Add component usage report 2026-01-18 12:32:39 +00:00
f428263a54 docs: add JSON component naming conventions 2026-01-18 12:32:07 +00:00
0ce4f6e7a4 Merge pull request #136 from johndoe6345789/codex/add-complex-expression-examples-and-guidance
Add guidance for complex JSON expressions
2026-01-18 12:30:51 +00:00
3b15b28059 Add guidance for JSON expression usage 2026-01-18 12:30:40 +00:00
1261c3e44d Merge pull request #135 from johndoe6345789/codex/extend-json-schema-with-new-components
Expand new molecules showcase loading examples
2026-01-18 12:28:05 +00:00
595aeb4df8 Expand new molecules showcase examples 2026-01-18 12:27:50 +00:00
c77753ee0a Merge pull request #134 from johndoe6345789/codex/add-integrity-check-for-supported-components
Add supported component validation in CI
2026-01-18 12:27:32 +00:00
a92c95c28a Add supported component validation check 2026-01-18 12:27:20 +00:00
82f572497c Merge pull request #133 from johndoe6345789/codex/review-and-map-missing-event-bindings
Document JSON binding gaps for components marked maybe-json-compatible
2026-01-18 12:27:03 +00:00
7922c14b7b Document JSON binding gaps for maybe-compatible components 2026-01-18 12:26:45 +00:00
4cf80e6fd8 Merge pull request #132 from johndoe6345789/codex/create-component-tracker-list
Add JSON components tracker and update status summary
2026-01-18 12:26:22 +00:00
575944fa0e Add JSON components tracker and status summary 2026-01-18 12:25:57 +00:00
df2c00dd06 Merge pull request #131 from johndoe6345789/codex/identify-json-compatible-components
Mark presentational molecules as high-priority JSON-ready in registry
2026-01-18 12:25:17 +00:00
0a1fe149d3 Merge branch 'main' into codex/identify-json-compatible-components 2026-01-18 12:25:09 +00:00
727a66218e Prioritize JSON-ready molecules in registry 2026-01-18 12:24:25 +00:00
29621b2765 Merge pull request #130 from johndoe6345789/codex/provide-sample-json-schemas-for-components
Add component schema templates (form, card, list) to JSON UI guide
2026-01-18 12:23:55 +00:00
0acd252ad0 Add component schema templates to JSON UI guide 2026-01-18 12:23:46 +00:00
2859d905ed Merge pull request #129 from johndoe6345789/codex/document-json-eligibility-checklist
Add JSON eligibility checklist to JSON_COMPONENTS.md
2026-01-18 12:23:21 +00:00
cf3f551698 Add JSON eligibility checklist 2026-01-18 12:23:03 +00:00
8f8305f95c Merge pull request #128 from johndoe6345789/codex/expand-json-component-support
Normalize json-components-registry and add JSON conversion implementation notes
2026-01-18 12:22:43 +00:00
bcd11011ad Normalize json components registry 2026-01-18 12:22:34 +00:00
bd9482b6d4 Merge pull request #127 from johndoe6345789/codex/create-wrapper-components-for-skipped-items
Move JSON wrapper interfaces to shared file
2026-01-18 12:18:45 +00:00
9e80117569 Fix wrapper interfaces import order 2026-01-18 12:18:33 +00:00
c2fc446f1f Merge pull request #126 from johndoe6345789/codex/register-circularprogress-and-divider
Register CircularProgress, Divider, and ProgressBar with JSON UI
2026-01-18 12:12:38 +00:00
13192f422e Register progress indicators for JSON UI 2026-01-18 12:10:45 +00:00
5b54fd3b2a Merge pull request #118 from johndoe6345789/codex/rename-component-definitions.ts-to-json
Convert component definitions to JSON
2026-01-18 12:08:48 +00:00
eab0c53210 Merge branch 'main' into codex/rename-component-definitions.ts-to-json 2026-01-18 12:08:38 +00:00
0042d2e2cd Merge pull request #124 from johndoe6345789/codex/add-datepicker-and-fileupload-components
Add DatePicker and FileUpload to JSON UI registry, types, and showcase
2026-01-18 12:00:21 +00:00
d952e1e9fc Merge branch 'main' into codex/add-datepicker-and-fileupload-components 2026-01-18 12:00:13 +00:00
0d13710c09 Add DatePicker and FileUpload to JSON UI 2026-01-18 11:59:51 +00:00
ef08246fc8 Merge pull request #122 from johndoe6345789/codex/add-json-ui-support-for-components
Add JSON UI support for CircularProgress, Divider, and ProgressBar
2026-01-18 11:54:57 +00:00
af58bcb7c2 Add JSON UI support for progress and divider 2026-01-18 11:54:28 +00:00
ae183ef80d Merge pull request #121 from johndoe6345789/codex/change-component-definitions.ts-to-json-file
Convert component definitions to JSON with typed helpers
2026-01-18 11:53:39 +00:00
53fdc3892d Convert component definitions to JSON 2026-01-18 11:53:08 +00:00
3031232ecf Convert component definitions to JSON 2026-01-18 11:43:37 +00:00
af03c13934 Merge pull request #117 from johndoe6345789/codex/integrate-breadcrumb-component-into-json
Add JSON Breadcrumb support (map to atoms/BreadcrumbNav)
2026-01-18 11:42:58 +00:00
4529708f76 Merge branch 'main' into codex/integrate-breadcrumb-component-into-json 2026-01-18 11:42:50 +00:00
8945c746cb Add breadcrumb to JSON component registry 2026-01-18 11:42:27 +00:00
2190be271f Merge pull request #116 from johndoe6345789/codex/convert-page-schemas.ts-to-json
Convert page schemas to JSON
2026-01-18 11:41:59 +00:00
e7fc49e53f Merge pull request #115 from johndoe6345789/codex/implement-prop-compatibility-for-components
Add JSON UI support for ErrorBadge, Notification, and StatusIcon
2026-01-18 11:41:22 +00:00
9448b8327d Merge branch 'main' into codex/implement-prop-compatibility-for-components 2026-01-18 11:41:15 +00:00
64c3b5b12b Convert page schemas to JSON 2026-01-18 11:40:46 +00:00
0d82406e5f Add JSON UI support for feedback atoms 2026-01-18 11:38:50 +00:00
233dbd2aa1 Merge pull request #114 from johndoe6345789/codex/implement-json-ui-component-integration
JSON UI: Add support for DataList / DataTable / MetricCard / Timeline
2026-01-18 11:36:19 +00:00
3fe02ed098 Add JSON UI support for data atoms 2026-01-18 11:35:57 +00:00
e7159916cb Merge pull request #113 from johndoe6345789/codex/assess-safety-of-converting-components-to-json
Add JSON component conversion task list
2026-01-18 11:32:36 +00:00
e41d08d40c Add JSON component conversion task list 2026-01-18 11:32:24 +00:00
754 changed files with 26233 additions and 14495 deletions

View File

@@ -0,0 +1,44 @@
{
"permissions": {
"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(npm run build:*)",
"Bash(npm install)",
"Bash(for file in ComponentBindingDialog.tsx DataSourceEditorDialog.tsx GitHubBuildStatus.tsx SaveIndicator.tsx ComponentTree.tsx SeedDataManager.tsx LazyD3BarChart.tsx StorageSettings.tsx)",
"Bash(do if [ -f \"$file\" ])",
"Bash(then echo \"EXISTS: $file\")",
"Bash(fi)",
"Bash(npm install:*)",
"Bash(for file in AppBranding BindingEditor Breadcrumb CanvasRenderer CodeExplanationDialog ComponentPalette)",
"Bash(do echo -n \"$file: \")",
"Bash(tsx scripts/audit-json-components.ts:*)",
"Bash(npm run audit:json:*)",
"Bash(bash:*)",
"Bash(git restore:*)",
"Bash(tree:*)",
"Bash(powershell:*)"
]
}
}

347
CLAUDE.md Normal file
View File

@@ -0,0 +1,347 @@
# Claude Code Documentation
## Architecture Overview
This is a low-code React application builder that is migrating from TypeScript/TSX components to a JSON-driven architecture.
### Current State (Jan 2026)
- **~420 TSX files** in `src/components/` (legacy - being phased out)
- **338 JSON definitions** in `src/config/pages/` (target architecture)
- **342 entries** in `json-components-registry.json`
- **19 complete JSON implementations** in `src/components/json-definitions/`
- **141 duplicate TSX files deleted** (had JSON equivalents)
- **5 atoms remaining** to convert: Accordion, FileUpload, Image, Menu, Popover
- **1 molecule remaining**: BindingEditor
- **3 organisms remaining**: DataSourceManager, NavigationMenu, TreeListPanel
## Migration Strategy
### Core Principle
**ALL components can be converted to JSON except the application entrypoint**, because custom hooks can handle any stateful/complex logic.
### Directory Structure
```
src/
├── components/ # 🔴 LEGACY - Phase out
│ ├── atoms/ # Basic UI components (6 TSX remaining)
│ │ ├── json-ui/ # JSON-specific atoms
│ │ ├── Accordion.tsx
│ │ ├── FileUpload.tsx
│ │ ├── Image.tsx
│ │ ├── Menu.tsx
│ │ └── Popover.tsx
│ ├── molecules/ # Composite components (1 TSX remaining)
│ │ └── BindingEditor.tsx
│ ├── organisms/ # Complex feature components (3 TSX remaining)
│ │ ├── DataSourceManager.tsx
│ │ ├── NavigationMenu.tsx
│ │ └── TreeListPanel.tsx
│ └── json-definitions/ # ✅ JSON implementations (19 files)
│ ├── loading-fallback.json
│ ├── navigation-item.json
│ ├── page-header-content.json
│ ├── component-binding-dialog.json
│ ├── data-source-editor-dialog.json
│ ├── github-build-status.json
│ ├── save-indicator.json
│ ├── component-tree.json
│ ├── seed-data-manager.json
│ ├── lazy-d3-bar-chart.json
│ ├── storage-settings.json
│ ├── tree-card.json
│ ├── filter-input.json
│ ├── copy-button.json
│ ├── input.json
│ └── password-input.json
├── config/
│ ├── pages/ # ✅ TARGET - JSON definitions (338 files)
│ │ ├── atoms/ # JSON schema for atoms
│ │ ├── molecules/ # JSON schema for molecules
│ │ ├── organisms/ # JSON schema for organisms
│ │ ├── templates/ # Page templates
│ │ └── *.json # Page definitions
│ └── pages.json # Central routing manifest
├── hooks/ # ✅ Custom hooks for JSON components
│ ├── use-save-indicator.ts
│ ├── use-component-tree.ts
│ ├── use-storage-backend-info.ts
│ ├── use-d3-bar-chart.ts
│ ├── use-focus-state.ts # NEW: For FilterInput
│ ├── use-copy-state.ts # NEW: For CopyButton
│ ├── use-password-visibility.ts # NEW: For PasswordInput
│ └── index.ts
├── lib/
│ └── json-ui/
│ ├── component-registry.ts # Component resolver
│ ├── component-renderer.tsx # JSON → React renderer
│ ├── json-components.ts # JSON component exports (27 components)
│ ├── create-json-component.tsx # Pure JSON component factory
│ ├── create-json-component-with-hooks.tsx # JSON + hooks factory
│ ├── hooks.ts # Data source/action hooks
│ ├── hooks-registry.ts # Hook registration (12 hooks registered)
│ ├── constants/ # Shared constants for JSON transforms
│ │ ├── sizes.ts # Button sizes, icon sizes, dimensions
│ │ ├── placements.ts # Popover/tooltip positioning
│ │ ├── styles.ts # Common CSS classes (transitions, animations, etc.)
│ │ ├── object-fit.ts # Image object-fit classes
│ │ └── index.ts
│ └── interfaces/ # TypeScript interfaces (1 per file)
│ ├── loading-fallback.ts
│ ├── navigation-item.ts
│ ├── page-header-content.ts
│ ├── save-indicator.ts
│ ├── lazy-bar-chart.ts
│ ├── lazy-line-chart.ts
│ ├── lazy-d3-bar-chart.ts
│ ├── seed-data-manager.ts
│ ├── storage-settings.ts
│ ├── github-build-status.ts
│ ├── component-binding-dialog.ts
│ ├── data-source-editor-dialog.ts
│ ├── component-tree.ts
│ ├── tree-card.ts
│ ├── filter-input.ts
│ ├── copy-button.ts
│ ├── input.ts
│ ├── password-input.ts
│ ├── image.ts
│ ├── popover.ts
│ ├── menu.ts
│ ├── file-upload.ts
│ ├── accordion.ts
│ └── index.ts
├── scripts/ # Migration and audit tools
│ ├── audit-json-components.ts
│ ├── analyze-duplicates.ts
│ ├── cleanup-registry.ts
│ └── fix-index-files.ts
└── json-components-registry.json # Master component registry
```
## How It Works
### 1. Routing Flow
```
pages.json → json-components-registry.json → Component Implementation
```
**Example:**
```json
// pages.json
{
"id": "dashboard",
"component": "ProjectDashboard"
}
// json-components-registry.json
{
"type": "ProjectDashboard",
"source": "organisms",
"load": {
"path": "@/components/ProjectDashboard",
"export": "ProjectDashboard"
}
}
```
### 2. Component Types
#### Pure JSON Components (No Hooks)
Simple stateless components defined entirely in JSON:
```json
// src/components/json-definitions/tree-card.json
{
"id": "tree-card-container",
"type": "Card",
"bindings": {
"className": {
"source": "isSelected",
"transform": "data ? 'ring-2 ring-primary' : 'hover:bg-accent/50'"
}
},
"children": [...]
}
```
Exported from `src/lib/json-ui/json-components.ts`:
```typescript
import treeCardDef from '@/components/json-definitions/tree-card.json'
export const TreeCard = createJsonComponent<TreeCardProps>(treeCardDef)
```
#### JSON Components with Hooks
Stateful components using custom hooks (**NO WRAPPER FILES NEEDED**):
```typescript
// src/lib/json-ui/json-components.ts
export const ComponentTree = createJsonComponentWithHooks<ComponentTreeProps>(
componentTreeDef,
{
hooks: {
treeData: {
hookName: 'useComponentTree',
args: (props) => [props.components || [], props.selectedId || null]
}
}
}
)
```
The custom hook is defined in `src/hooks/use-component-tree.ts` (or other hook files) and registered in `src/lib/json-ui/hooks-registry.ts`.
#### TSX Components (Legacy)
Currently imported directly - these need migration:
```typescript
// ❌ OLD: Direct TSX import
import { AppBranding } from '@/components/molecules/AppBranding'
// ✅ NEW: JSON-based import
import { AppBranding } from '@/lib/json-ui/json-components'
```
### 3. Registry System
The `json-components-registry.json` defines how components are loaded:
```json
{
"type": "SaveIndicator",
"source": "molecules",
"jsonCompatible": true
}
```
- **jsonCompatible**: Whether component can be expressed as JSON
- **load.path**: Explicit path to component file (for TSX legacy components)
- **source**: Where the component comes from (atoms, molecules, organisms, ui)
**Note:** `wrapperRequired` and `wrapperComponent` fields in the registry are **obsolete** and should be removed. All stateful logic is handled via `createJsonComponentWithHooks`.
## Current Issues (Jan 2026)
### Audit Results
Run `npm run audit:json` to see current status:
-**Errors**
- 6 orphaned JSON files (no registry entry)
- 7 broken load paths
- ⚠️ **153 warnings**
- 153 duplicate implementations (TSX + JSON)
### Critical Tasks
1. **Phase Out `src/components/`**
- 153 components have both TSX and JSON definitions
- TSX versions should be deleted and routed through JSON
2. **Clean Up Registry**
- Remove `wrapperRequired` and `wrapperComponent` fields (obsolete)
- All stateful logic is handled via `createJsonComponentWithHooks`
- Custom hooks defined in `src/lib/json-ui/hooks.ts`
3. **Fix Registry Issues**
- Add missing registry entries for orphaned JSON
- Fix broken load paths
- Verify all source mappings
## Migration Checklist
For each component:
- [ ] Create JSON definition in `src/components/json-definitions/`
- [ ] Add TypeScript interface in `src/lib/json-ui/interfaces/` (one file per interface)
- [ ] If stateful: Define custom hook in `src/hooks/use-[component-name].ts`
- [ ] If stateful: Register hook in `src/lib/json-ui/hooks-registry.ts`
- [ ] If stateful: Export hook from `src/hooks/index.ts`
- [ ] Export from `src/lib/json-ui/json-components.ts`:
- Use `createJsonComponent` for pure/stateless
- Use `createJsonComponentWithHooks` for stateful
- [ ] Update registry in `json-components-registry.json`
- [ ] Update all imports to use `@/lib/json-ui/json-components`
- [ ] Delete legacy TSX file from `src/components/`
- [ ] Run tests and build to verify
## Useful Commands
```bash
# Run audit to check migration status
npm run audit:json
# Generate component types
npm run components:generate-types
# Build (will fail if components missing)
npm run build
```
## Key Files
- `json-components-registry.json` - Master registry of all components
- `src/config/pages.json` - Page routing configuration
- `src/lib/json-ui/component-registry.ts` - Component resolver logic
- `src/lib/json-ui/json-components.ts` - JSON component exports
- `src/lib/json-ui/hooks.ts` - Custom hooks for stateful components
- `src/lib/json-ui/hooks-registry.ts` - Hook registration
- `scripts/audit-json-components.ts` - Audit tool
## Notes
- **Never create new TSX components** - use JSON instead
- **All components can be JSON** except the app entrypoint
- **Use custom hooks** for stateful logic (via `createJsonComponentWithHooks`)
- **NO wrapper files needed** - hooks are defined in `hooks.ts` and registered in `hooks-registry.ts`
- **One interface per file** in `src/lib/json-ui/interfaces/`
- **Meta JSON files** in `src/config/pages/` are routing schemas
- **Full JSON definitions** live in `src/components/json-definitions/`
## Recent Changes (Jan 2026)
### Phase 1: Setup & Cleanup
- ✅ Fixed e2e build failures (TreeCard, TreeListHeader routing)
- ✅ Removed 8 initial duplicate TSX files with JSON equivalents
- ✅ Split wrapper-interfaces.ts into individual interface files
- ✅ Created audit script to track migration progress
- ✅ Updated imports to use `@/lib/json-ui/json-components`
- ✅ Clarified: NO wrapper system - use JSON + custom hooks
### Phase 2: Mass Cleanup
- ✅ Cleaned registry - removed 107 obsolete `wrapperRequired`/`wrapperComponent` fields
- ✅ Analyzed 153 duplicates, categorized safe deletions
- ✅ Deleted 141 duplicate TSX files (had complete JSON implementations)
- ✅ Created fix-index-files.ts script to auto-update exports
### Phase 3: Active Conversions (In Progress)
- ✅ Converted FilterInput to JSON with useFocusState hook
- ✅ Converted CopyButton to JSON with useCopyState hook
- ✅ Converted Input to JSON (pure component with forwardRef support)
- ✅ Converted PasswordInput to JSON with usePasswordVisibility hook
- ✅ Moved custom hooks from `lib/json-ui/hooks.ts` to `src/hooks/` directory
- ✅ Created use-focus-state.ts, use-copy-state.ts, and use-password-visibility.ts
- ✅ Updated hooks-registry.ts to include 7 registered hooks
### Remaining Work
- 🔄 5 atoms left: Accordion, FileUpload, Image, Menu, Popover
- 🔄 1 molecule left: BindingEditor
- 🔄 3 organisms left: DataSourceManager, NavigationMenu, TreeListPanel
- ✅ 20 JSON components complete (up from 12)
## Next Steps
1. Clean up registry - remove `wrapperRequired` and `wrapperComponent` fields
2. Convert the 153 duplicate TSX components to JSON-only
3. Fix 6 orphaned JSON files (add registry entries)
4. Fix 7 broken load paths in registry
5. Complete full migration of `src/components/` to JSON

59
CLAUDE_UPDATE.md Normal file
View File

@@ -0,0 +1,59 @@
# Update for Current State section (lines 8-15)
- **~400 TSX files** in `src/components/` (legacy - being phased out)
- **338 JSON definitions** in `src/config/pages/` (target architecture)
- **342 entries** in `json-components-registry.json`
- **27 complete JSON implementations** in `src/components/json-definitions/`
- **141 duplicate TSX files deleted** (had JSON equivalents)
- **✅ ALL ATOMS CONVERTED!** (0 remaining)
- **1 molecule remaining**: BindingEditor
- **3 organisms remaining**: DataSourceManager, NavigationMenu, TreeListPanel
# Update for atoms section (lines 28-34)
│ ├── atoms/ # ✅ ALL CONVERTED! (0 TSX remaining)
│ │ └── json-ui/ # JSON-specific atoms
# Update for json-definitions (lines 41-57)
│ └── json-definitions/ # ✅ JSON implementations (27 files)
│ ├── loading-fallback.json
│ ├── navigation-item.json
│ ├── page-header-content.json
│ ├── component-binding-dialog.json
│ ├── data-source-editor-dialog.json
│ ├── github-build-status.json
│ ├── save-indicator.json
│ ├── component-tree.json
│ ├── seed-data-manager.json
│ ├── lazy-d3-bar-chart.json
│ ├── storage-settings.json
│ ├── tree-card.json
│ ├── filter-input.json
│ ├── copy-button.json
│ ├── input.json
│ ├── password-input.json
│ ├── image.json
│ ├── popover.json
│ ├── menu.json
│ ├── file-upload.json
│ └── accordion.json
# Update for hooks (lines 73-76)
│ ├── use-focus-state.ts # For FilterInput
│ ├── use-copy-state.ts # For CopyButton
│ ├── use-password-visibility.ts # For PasswordInput
│ ├── use-image-state.ts # For Image
│ ├── use-popover-state.ts # For Popover
│ ├── use-menu-state.ts # For Menu
│ ├── use-file-upload.ts # For FileUpload
│ ├── use-accordion.ts # For Accordion
# Update for json-components count (line 82)
│ ├── json-components.ts # JSON component exports (27 components)
# Update for hooks-registry count (line 86)
│ ├── hooks-registry.ts # Hook registration (12 hooks registered)

View File

@@ -1,164 +0,0 @@
# JSON Compatibility Implementation Summary
## Overview
This document summarizes the low-hanging fruit implemented from the JSON_COMPATIBILITY_ANALYSIS.md document.
## ✅ Completed Work
### 1. Added 6 Molecular Components to JSON Registry
The following components have been successfully integrated into the JSON UI system:
#### Components Added:
1. **AppBranding** - Application branding with logo, title, and subtitle
2. **LabelWithBadge** - Label with optional badge indicator (supports variant customization)
3. **EmptyEditorState** - Empty state display for editor contexts
4. **LoadingFallback** - Loading message display with spinner
5. **LoadingState** - Configurable loading state indicator (supports size variants)
6. **NavigationGroupHeader** - Navigation group header with expand/collapse indicator
### 2. Updated Type Definitions
**File: `src/types/json-ui.ts`**
- Added all 6 new component types to the `ComponentType` union type
- Ensures full TypeScript support for the new components in JSON schemas
### 3. Updated Component Registry
**File: `src/lib/json-ui/component-registry.tsx`**
- Added imports for all 6 new molecular components
- Registered components in `componentRegistry` object
- Added components to `customComponents` export for enhanced discoverability
### 4. Created Showcase Schema
**File: `src/schemas/page-schemas.ts`**
- Created `newMoleculesShowcaseSchema` - A comprehensive demonstration page
- Showcases each new component with realistic use cases
- Includes data bindings and multiple variants
- Demonstrates integration within Card layouts
### 5. Enhanced JSON UI Showcase Page
**File: `src/components/JSONUIShowcasePage.tsx`**
- Added new "New Molecules" tab to the showcase
- Integrated the new showcase schema with PageRenderer
- Provides instant visual verification of the new components
## 📊 Impact
### Before:
- JSON-compatible molecules: 3 (DataCard, SearchInput, ActionBar)
- Total JSON components: ~60 (mostly atoms and UI primitives)
### After:
- JSON-compatible molecules: 9 (+6 new)
- Total JSON components: ~66 (+10% increase)
- Enhanced showcase with dedicated demonstration page
## 🎯 Components Analysis Results
From the original 13 "fully compatible" molecules identified:
| Component | Status | Reason |
|-----------|--------|--------|
| AppBranding | ✅ Added | Simple props, no state |
| LabelWithBadge | ✅ Added | Simple props, no state |
| EmptyEditorState | ✅ Added | No props, pure display |
| LoadingFallback | ✅ Added | Simple props, no state |
| LoadingState | ✅ Added | Simple props, no state |
| NavigationGroupHeader | ✅ Added | Simple props, display-only |
| Breadcrumb | ❌ Skipped | Uses hooks (useNavigationHistory) |
| SaveIndicator | ❌ Skipped | Internal state + useEffect |
| LazyBarChart | ❌ Skipped | Uses async hooks (useRecharts) |
| LazyD3BarChart | ❌ Skipped | Uses async hooks |
| LazyLineChart | ❌ Skipped | Uses async hooks |
| SeedDataManager | ❌ Skipped | Complex hooks + event handlers |
| StorageSettings | ❌ Skipped | Complex state + side effects |
**Success Rate: 6/13 (46%)** - Realistic assessment based on actual complexity
## 📝 Usage Example
Here's how to use the new components in JSON schemas:
```json
{
"id": "my-component",
"type": "AppBranding",
"props": {
"title": "My Application",
"subtitle": "Powered by JSON"
}
}
```
```json
{
"id": "label-with-count",
"type": "LabelWithBadge",
"props": {
"label": "Active Users",
"badgeVariant": "default"
},
"bindings": {
"badge": { "source": "userCount" }
}
}
```
```json
{
"id": "empty-state",
"type": "EmptyEditorState",
"props": {}
}
```
```json
{
"id": "loading",
"type": "LoadingState",
"props": {
"message": "Loading your data...",
"size": "md"
}
}
```
## 🔄 Next Steps
### Immediate Opportunities:
1. **Chart Components** - Create simplified wrapper components for charts that don't require hooks
2. **Event Binding System** - Implement the event binding system described in the analysis
3. **State Binding System** - Implement the state binding system for interactive components
4. **Component Wrappers** - Create JSON-friendly wrappers for complex existing components
### Medium-term Goals:
1. Add the 27 "maybe compatible" molecules with event binding support
2. Implement computed prop transformations for dynamic component behavior
3. Create JSON-friendly versions of the 14 organisms
4. Build a visual component palette showing all JSON-compatible components
## 📚 Documentation
- Main analysis: `JSON_COMPATIBILITY_ANALYSIS.md`
- Implementation summary: `JSON_COMPATIBILITY_IMPLEMENTATION.md` (this file)
- Component registry: `src/lib/json-ui/component-registry.tsx`
- Type definitions: `src/types/json-ui.ts`
- Showcase schema: `src/schemas/page-schemas.ts`
- Live demo: Navigate to JSON UI Showcase → "New Molecules" tab
## ✨ Key Achievements
1. ✅ Successfully identified and added truly simple JSON-compatible components
2. ✅ Maintained type safety throughout the implementation
3. ✅ Created comprehensive demonstration with real-world examples
4. ✅ Updated all relevant documentation
5. ✅ Provided clear path forward for future additions
## 🎉 Conclusion
We successfully implemented the low-hanging fruit from the JSON compatibility analysis, adding 6 new molecular components to the JSON UI registry. These components are now fully usable in JSON schemas and have been demonstrated in the enhanced showcase page.
The implementation prioritized truly simple components without complex dependencies, hooks, or state management, ensuring reliable JSON-driven rendering. The remaining "fully compatible" components were correctly identified as requiring additional infrastructure (hooks, state management) that makes them unsuitable for pure JSON configuration without wrapper components.

View File

@@ -1,210 +0,0 @@
# JSON UI Components Registry
This document describes the JSON UI component system and lists all components that can be rendered from JSON schemas.
## Overview
The JSON UI system allows you to define user interfaces using JSON schemas instead of writing React code. This is useful for:
- Dynamic UI generation
- No-code/low-code interfaces
- Configuration-driven UIs
- Rapid prototyping
## Quick Start
### List All JSON-Compatible Components
```bash
# List all components with details
npm run components:list
# List only supported components
npm run components:list -- --status=supported
# List only planned components
npm run components:list -- --status=planned
# Output as JSON
npm run components:list -- --format=json
```
### Using JSON UI Components
Components are defined in the `ComponentType` union in `src/types/json-ui.ts` and registered in `src/lib/json-ui/component-registry.tsx`.
Example JSON schema:
```json
{
"id": "example-page",
"type": "Card",
"props": {
"className": "p-6"
},
"children": [
{
"id": "heading",
"type": "Heading",
"props": {
"level": 2,
"children": "Welcome"
}
},
{
"id": "description",
"type": "Text",
"props": {
"children": "This is a dynamically rendered component"
}
},
{
"id": "cta",
"type": "Button",
"props": {
"variant": "default",
"children": "Get Started"
}
}
]
}
```
## Component Categories
### Layout Components (12)
Container elements for organizing content:
- `div`, `section`, `article`, `header`, `footer`, `main` - HTML semantic elements
- `Card` - Container with optional header, content, and footer
- `Grid` - Responsive grid layout
- `Stack` - Vertical or horizontal stack layout
- `Flex` - Flexible box layout
- `Container` - Centered container with max-width
- `Dialog` - Modal dialog overlay
### Input Components (11)
Form inputs and interactive controls:
- `Button` - Interactive button
- `Input` - Text input field
- `TextArea` - Multi-line text input
- `Select` - Dropdown select
- `Checkbox` - Checkbox toggle
- `Radio` - Radio button
- `Switch` - Toggle switch
- `Slider` - Numeric range slider
- `NumberInput` - Numeric input with increment/decrement
- `DatePicker` - Date selection (planned)
- `FileUpload` - File upload control (planned)
### Display Components (16)
Presentation and visual elements:
- `Heading` - Heading text (h1-h6)
- `Text` - Text content with typography
- `Label` - Form label
- `Badge` - Status or count indicator
- `Tag` - Removable tag/chip
- `Code` - Inline or block code
- `Image` - Image with loading states
- `Avatar` - User avatar image
- `Icon` - Icon from library (planned)
- `Progress` - Progress bar
- `Spinner` - Loading spinner
- `Skeleton` - Loading placeholder
- `Separator` - Visual divider
- `CircularProgress` - Circular indicator (planned)
- `ProgressBar` - Linear progress (planned)
- `Divider` - Section divider (planned)
### Navigation Components (3)
Navigation and routing:
- `Link` - Hyperlink element
- `Breadcrumb` - Navigation trail (planned)
- `Tabs` - Tabbed interface
### Feedback Components (7)
Alerts, notifications, and status:
- `Alert` - Alert notification message
- `InfoBox` - Information box with icon
- `EmptyState` - Empty state placeholder
- `StatusBadge` - Status indicator
- `StatusIcon` - Status icon (planned)
- `ErrorBadge` - Error state (planned)
- `Notification` - Toast notification (planned)
### Data Components (8)
Data display and visualization:
- `List` - Generic list renderer
- `Table` - Data table
- `KeyValue` - Key-value pair display
- `StatCard` - Statistic card
- `DataList` - Styled data list (planned)
- `DataTable` - Advanced table with sorting/filtering (planned)
- `Timeline` - Timeline visualization (planned)
- `MetricCard` - Metric display (planned)
### Custom Components (3)
Domain-specific components:
- `DataCard` - Custom data display card
- `SearchInput` - Search input with icon
- `ActionBar` - Action button toolbar
## Current Status
- **Total Components**: 60
- **Supported**: 46 (77%)
- **Planned**: 14 (23%)
## Files
- `json-components-registry.json` - Complete registry with metadata
- `src/types/json-ui.ts` - TypeScript types and ComponentType union
- `src/lib/json-ui/component-registry.tsx` - Component registry mapping
- `src/lib/component-definitions.ts` - Component definitions with defaults
- `scripts/list-json-components.cjs` - CLI tool to list components
## Adding New Components
To add a new component to the JSON UI system:
1. Add the component type to `ComponentType` union in `src/types/json-ui.ts`
2. Import and register it in `src/lib/json-ui/component-registry.tsx`
3. Add component definition in `src/lib/component-definitions.ts`
4. Update `json-components-registry.json` with metadata
5. Test the component in a JSON schema
### JSON Compatibility Checklist
Before migrating a component, confirm all required conditions are met:
- [ ] **Hooks/state are registry-safe**: hooks and internal state are acceptable when the component registry can control or expose them through JSON bindings.
- [ ] **Bindings are defined**: any required actions, event handlers, or state bindings are already supported by the JSON UI binding system.
- [ ] **Refactoring covered by PR**: JSON compatibility gaps should be resolved via refactoring as part of the same pull request.
### Step-by-Step Migration Path
Use this repeatable migration flow for planned components:
1. **Update the type union** in `src/types/json-ui.ts` to include the new component type name.
2. **Register the component** in `src/lib/json-ui/component-registry.tsx` so JSON schemas can resolve it at runtime.
3. **Define component metadata** in `src/lib/component-definitions.ts` (defaults, prop schema, and any JSON-driven constraints).
4. **Validate JSON schema usage** by rendering a sample schema that uses the new type.
5. **Update registry metadata** in `json-components-registry.json` so the CLI/listing reflects the new status.
## Migration Strategy
Components marked as "planned" are:
- Available in the codebase as React components
- Not yet integrated into the JSON UI system
- Can be migrated following the steps above
Priority for migration:
1. High-usage components
2. Components with simple props
3. Components with good atomic design
4. Components without complex state management
## Related Documentation
- [PRD.md](./PRD.md) - Product requirements document
- [REDUX_DOCUMENTATION.md](./REDUX_DOCUMENTATION.md) - Redux integration
- [src/types/json-ui.ts](./src/types/json-ui.ts) - Type definitions
- [src/lib/component-definitions.ts](./src/lib/component-definitions.ts) - Component metadata

View File

@@ -1,110 +1,36 @@
# JSON Expression System
This document describes the JSON-friendly expression system for handling events without requiring external TypeScript functions.
This document describes the supported JSON expression patterns used across JSON UI schemas.
Legacy compute functions have been removed in favor of expression strings and value templates.
## Overview
## Core Concepts
The JSON Expression System allows you to define dynamic behaviors entirely within JSON schemas, eliminating the need for external compute functions. This makes schemas more portable and easier to edit.
### Expressions
## Expression Types
### 1. Simple Expressions
Use the `expression` field to evaluate dynamic values:
Expressions are string values that resolve against a data + event context:
```json
{
"type": "set-value",
"target": "username",
"expression": "event.target.value"
}
```
**Supported Expression Patterns:**
Supported expression patterns:
- **Data Access**: `"data.fieldName"`, `"data.user.name"`, `"data.items.0.id"`
- Access any field in the data context
- Supports nested objects using dot notation
- `data` or `event`
- Dot access: `data.user.name`, `event.target.value`
- Literals: numbers, booleans, `null`, `undefined`, quoted strings
- Time: `Date.now()`
- Array filtering:
- `data.todos.filter(completed === true)`
- `data.users.filter(status === 'active').length`
- **Event Access**: `"event.target.value"`, `"event.key"`, `"event.type"`
- Access event properties
- Commonly used for form inputs
### Value Templates
- **Date Operations**: `"Date.now()"`
- Get current timestamp
- Useful for creating unique IDs
- **Literals**: `42`, `"hello"`, `true`, `false`, `null`
- Direct values
### 2. Value Templates
Use the `valueTemplate` field to create objects with dynamic values:
Value templates are JSON objects whose string values are evaluated as expressions:
```json
{
"type": "create",
"target": "todos",
"valueTemplate": {
"id": "Date.now()",
"text": "data.newTodo",
"completed": false,
"createdBy": "data.currentUser"
}
}
```
**Template Behavior:**
- String values starting with `"data."` or `"event."` are evaluated as expressions
- Other values are used as-is
- Perfect for creating new objects with dynamic fields
### 3. Static Values
Use the `value` field for static values:
```json
{
"type": "set-value",
"target": "isLoading",
"value": false
}
```
## Action Types with Expression Support
### set-value
Update a data source with a new value.
**With Expression:**
```json
{
"id": "update-filter",
"type": "set-value",
"target": "searchQuery",
"expression": "event.target.value"
}
```
**With Static Value:**
```json
{
"id": "reset-filter",
"type": "set-value",
"target": "searchQuery",
"value": ""
}
```
### create
Add a new item to an array data source.
**With Value Template:**
```json
{
"id": "add-todo",
"type": "create",
"target": "todos",
"valueTemplate": {
"id": "Date.now()",
"text": "data.newTodo",
@@ -113,210 +39,24 @@ Add a new item to an array data source.
}
```
### update
Update an existing value (similar to set-value).
### Conditions
Conditions use expression strings that are evaluated against the data context:
```json
{
"id": "update-count",
"type": "update",
"target": "viewCount",
"expression": "data.viewCount + 1"
"condition": "data.newTodo.length > 0"
}
```
**Note:** Arithmetic expressions are not yet supported. Use `increment` action type instead.
Supported condition patterns:
### delete
Remove an item from an array.
- `data.field > 0`
- `data.field.length > 0`
- `data.field === 'value'`
- `data.field != null`
```json
{
"id": "remove-todo",
"type": "delete",
"target": "todos",
"path": "id",
"expression": "data.selectedId"
}
```
## Legacy Compute Functions (Removed)
## Common Patterns
### 1. Input Field Updates
```json
{
"id": "name-input",
"type": "Input",
"bindings": {
"value": { "source": "userName" }
},
"events": [
{
"event": "change",
"actions": [
{
"type": "set-value",
"target": "userName",
"expression": "event.target.value"
}
]
}
]
}
```
### 2. Creating Objects with IDs
```json
{
"type": "create",
"target": "items",
"valueTemplate": {
"id": "Date.now()",
"name": "data.newItemName",
"status": "pending",
"createdAt": "Date.now()"
}
}
```
### 3. Resetting Forms
```json
{
"event": "click",
"actions": [
{
"type": "set-value",
"target": "formField1",
"value": ""
},
{
"type": "set-value",
"target": "formField2",
"value": ""
}
]
}
```
### 4. Success Notifications
```json
{
"type": "show-toast",
"message": "Item saved successfully!",
"variant": "success"
}
```
## Backward Compatibility
The system maintains backward compatibility with the legacy `compute` function approach:
**Legacy (still supported):**
```json
{
"type": "set-value",
"target": "userName",
"compute": "updateUserName"
}
```
**New (preferred):**
```json
{
"type": "set-value",
"target": "userName",
"expression": "event.target.value"
}
```
The schema loader will automatically hydrate legacy `compute` references while new schemas can use pure JSON expressions.
## Limitations
Current limitations (may be addressed in future updates):
1. **No Arithmetic**: Cannot do `"data.count + 1"` - use `increment` action type instead
2. **No String Concatenation**: Cannot do `"Hello " + data.name` - use template strings in future
3. **No Complex Logic**: Cannot do nested conditionals or loops
4. **No Custom Functions**: Cannot call user-defined functions
For complex logic, you can still use the legacy `compute` functions or create custom action types.
## Migration Guide
### From Compute Functions to Expressions
**Before:**
```typescript
// In compute-functions.ts
export const updateNewTodo = (data: any, event: any) => event.target.value
// In schema
{
"type": "set-value",
"target": "newTodo",
"compute": "updateNewTodo"
}
```
**After:**
```json
{
"type": "set-value",
"target": "newTodo",
"expression": "event.target.value"
}
```
**Before:**
```typescript
// In compute-functions.ts
export const computeAddTodo = (data: any) => ({
id: Date.now(),
text: data.newTodo,
completed: false,
})
// In schema
{
"type": "create",
"target": "todos",
"compute": "computeAddTodo"
}
```
**After:**
```json
{
"type": "create",
"target": "todos",
"valueTemplate": {
"id": "Date.now()",
"text": "data.newTodo",
"completed": false
}
}
```
## Examples
See the example schemas:
- `/src/schemas/todo-list-json.json` - Pure JSON event system example
- `/src/schemas/todo-list.json` - Legacy compute function approach
## Future Enhancements
Planned features for future versions:
1. **Arithmetic Expressions**: `"data.count + 1"`
2. **String Templates**: `"Hello ${data.userName}"`
3. **Comparison Operators**: `"data.age > 18"`
4. **Logical Operators**: `"data.isActive && data.isVerified"`
5. **Array Operations**: `"data.items.filter(...)"`, `"data.items.map(...)"`
6. **String Methods**: `"data.text.trim()"`, `"data.email.toLowerCase()"`
For now, use the legacy `compute` functions for these complex scenarios.
Schemas should no longer reference function names in `compute`, `transform`, or string-based
condition fields. Use `expression` and `valueTemplate` instead.

9
Jenkinsfile vendored
View File

@@ -68,6 +68,15 @@ pipeline {
}
}
}
stage('Component Registry Check') {
steps {
script {
nodejs(nodeJSInstallationName: "Node ${NODE_VERSION}") {
sh 'npm run components:validate'
}
}
}
}
}
}

View File

@@ -0,0 +1,277 @@
# Review: maybe-json-compatible components and binding gaps
## Scope
Components still marked `maybe-json-compatible` were reviewed for missing event/state bindings that would need to be exposed to the JSON UI system. This list mirrors the registry entries that currently sit in that status. Each component below is annotated with the missing bindings that should be mapped to JSON events (`events`) or data bindings (`bindings`/`dataBinding`).
## Component-by-component binding gaps
### Dialogs and editor flows
- **CodeExplanationDialog**: needs JSON bindings for `open` and `onOpenChange`, plus data bindings for `fileName`, `explanation`, and `isLoading` so schemas can control dialog visibility and content. These are currently prop-only.
- **ComponentBindingDialog**: needs JSON bindings for `open`, `component`, and `dataSources`, plus event bindings for `onOpenChange` and `onSave`. This dialog also pipes `onChange` updates through `BindingEditor`, which should map to JSON actions when used from schemas.
- **DataSourceEditorDialog**: needs JSON bindings for `open`, `dataSource`, `allDataSources`, plus event bindings for `onOpenChange` and `onSave`. Internally, field updates (e.g., `updateField`, dependency add/remove) are not yet exposed as JSON actions.
- **TreeFormDialog**: needs JSON bindings for `open`, `name`, `treeDescription`, plus event bindings for `onNameChange`, `onDescriptionChange`, `onOpenChange`, and `onSubmit`.
### Selection and list management
- **FileTabs**: needs JSON bindings for `files` and `activeFileId`, plus event bindings for `onFileSelect` and `onFileClose`.
- **NavigationItem**: needs JSON binding for `isActive`/`badge` and event binding for `onClick`.
- **NavigationMenu**: relies on internal `expandedGroups` state and a set of callbacks (`onTabChange`, `onToggleGroup`, `onItemHover`, `onItemLeave`). These should be exposed as JSON data bindings and events to support JSON-driven navigation and hover-driven actions (e.g., preloading routes).
- **TreeCard**: needs event bindings for `onSelect`, `onEdit`, `onDuplicate`, and `onDelete` plus data bindings for `isSelected`/`disableDelete` to allow schema-driven selection state.
- **TreeListHeader**: needs event bindings for `onCreateNew`, `onImportJson`, and `onExportJson`, with `hasSelectedTree` coming from data bindings.
- **TreeListPanel**: orchestrates tree selection and CRUD; bindings are needed for `trees`, `selectedTreeId`, and event callbacks (`onTreeSelect`, `onTreeEdit`, `onTreeDuplicate`, `onTreeDelete`, `onCreateNew`, `onImportJson`, `onExportJson`).
### Data source management
- **DataSourceCard**: requires event bindings for `onEdit` and `onDelete`, plus data bindings for `dataSource` and `dependents`.
- **DataSourceManager**: uses local state for `editingSource` and dialog visibility while exposing `onChange` externally. Needs JSON bindings for `dataSources` and events for `onAdd`, `onEdit`, `onDelete`, `onSave` (mapped to create/update/delete actions) plus ability to toggle dialog state from JSON.
### Editor UI and property panels
- **BindingEditor**: should expose `bindings`, `dataSources`, and `availableProps` through data bindings plus event bindings for `onChange` when bindings are added/removed.
- **CanvasRenderer**: needs JSON events for `onSelect`, `onHover`, `onHoverEnd`, `onDragOver`, `onDragLeave`, and `onDrop`, and data bindings for `selectedId`, `hoveredId`, `draggedOverId`, and `dropPosition` so drag/hover state can live in JSON data.
- **ComponentPalette**: should expose `onDragStart` via JSON events, and optionally a binding for the active tab/category if schemas should control which tab is open.
- **ComponentTree**: relies on internal expansion state (`expandedIds`) and emits `onSelect`, `onHover`, `onDragStart`, `onDrop`, etc. Those should be JSON event bindings plus data bindings for expansion and selection state.
- **PropertyEditor**: needs event bindings for `onUpdate` and `onDelete`, with the selected `component` coming from JSON data.
- **SchemaEditorCanvas**: mirrors `CanvasRenderer`; bindings needed for all selection/hover/drag data and events.
- **SchemaEditorLayout**: orchestrates `onImport`, `onExport`, `onCopy`, `onPreview`, `onClear`, plus component drag events and selection state. These should map to JSON action handlers.
- **SchemaEditorPropertiesPanel**: inherits `ComponentTree` and `PropertyEditor` events; all selection/drag/update/delete events should be exposed in JSON.
- **SchemaEditorSidebar**: needs JSON event binding for `onDragStart` from the component palette.
- **SchemaEditorToolbar**: needs JSON event bindings for `onImport`, `onExport`, `onCopy`, `onPreview`, and `onClear`.
### Search and toolbar interactions
- **ActionBar**: actions array needs JSON event bindings for each `onClick` with optional `disabled`/`variant` driven by bindings.
- **EditorActions**: needs JSON event bindings for `onExplain` and `onImprove`.
- **EditorToolbar**: needs bindings for `openFiles` and `activeFileId`, plus events for file select/close and explain/improve actions.
- **SearchBar**: needs binding for `value` plus event binding for `onChange`/clear.
- **SearchInput**: needs binding for `value` plus event bindings for `onChange` and `onClear`.
- **ToolbarButton** and **ToolbarActions**: need JSON event bindings for their `onClick` handlers.
### Monaco editor integrations
- **LazyInlineMonacoEditor**: needs data binding for `value` and event binding for `onChange`.
- **LazyMonacoEditor**/**MonacoEditorPanel**: same binding as above (value/content and change events).
### Mostly presentational components (no missing event/state bindings beyond data)
These components are largely render-only and should work with basic `props`/`bindings` without extra event wiring: **SchemaCodeViewer**, **EmptyCanvasState**, **EmptyState**, **SchemaEditorStatusBar**, **StatCard**, **DataCard**, **PageHeaderContent**, **AppHeader** (except for the actions passed into the toolbar components), **JSONUIShowcase** (internal demo state).
## Mapping missing bindings to the JSON action + expression systems
The JSON UI system already supports `events` for action execution and `bindings`/`dataBinding` for state. The following mappings show how each missing binding should be wired.
### 1) Dialog open/close control
**Bindings:** `open` state stored in a data source.
```json
{
"id": "code-explain-dialog",
"type": "CodeExplanationDialog",
"bindings": {
"open": { "source": "uiState", "path": "dialogs.codeExplainOpen" },
"fileName": { "source": "editor", "path": "activeFile.name" },
"explanation": { "source": "ai", "path": "explanation" },
"isLoading": { "source": "ai", "path": "loading" }
},
"events": {
"onOpenChange": {
"actions": [
{
"id": "toggle-code-explain",
"type": "set-value",
"target": "uiState.dialogs.codeExplainOpen",
"expression": "event"
}
]
}
}
}
```
**Why:** `onOpenChange` provides a boolean; the JSON action `set-value` with an expression is a direct mapping for controlled dialog visibility.
### 2) Input value + change events (SearchBar/SearchInput/TreeFormDialog)
**Bindings:** `value` and `onChange` mapped to `set-value` with `event.target.value`.
```json
{
"id": "search-input",
"type": "SearchInput",
"bindings": {
"value": { "source": "filters", "path": "query" }
},
"events": {
"onChange": {
"actions": [
{
"id": "update-search-query",
"type": "set-value",
"target": "filters.query",
"expression": "event.target.value"
}
]
},
"onClear": {
"actions": [
{
"id": "clear-search-query",
"type": "set-value",
"target": "filters.query",
"value": ""
}
]
}
}
}
```
**Why:** `event.target.value` is supported by the JSON expression system, allowing direct mapping from inputs.
### 3) List selection (FileTabs, NavigationMenu, TreeListPanel)
**Bindings:** selection ID stored in state, `onClick` mapped to `set-value` with a static or computed value.
```json
{
"id": "file-tabs",
"type": "FileTabs",
"bindings": {
"files": { "source": "editor", "path": "openFiles" },
"activeFileId": { "source": "editor", "path": "activeFileId" }
},
"events": {
"onFileSelect": {
"actions": [
{
"id": "select-file",
"type": "set-value",
"target": "editor.activeFileId",
"expression": "event"
}
]
},
"onFileClose": {
"actions": [
{
"id": "close-file",
"type": "custom",
"params": { "fileId": "event" }
}
]
}
}
}
```
**Why:** selection changes are simple state updates. More complex close behavior can map to a `custom` action if it needs side effects.
### 4) Toolbar and button actions (ActionBar, ToolbarActions, EditorActions)
**Bindings:** each `onClick` maps to a JSON action list.
```json
{
"id": "schema-toolbar",
"type": "SchemaEditorToolbar",
"events": {
"onImport": { "actions": [{ "id": "import-json", "type": "custom" }] },
"onExport": { "actions": [{ "id": "export-json", "type": "custom" }] },
"onCopy": { "actions": [{ "id": "copy-json", "type": "custom" }] },
"onPreview": { "actions": [{ "id": "open-preview", "type": "open-dialog", "target": "uiState", "path": "preview" }] },
"onClear": { "actions": [{ "id": "clear-schema", "type": "set-value", "target": "schema.components", "value": [] }] }
}
}
```
**Why:** these are pure event triggers; `custom` actions cover app-specific flows that arent part of the built-in action types.
**Dialog storage convention:** `open-dialog`/`close-dialog` actions store booleans in `uiState.dialogs.<dialogId>`. Use `target` for the data source (typically `uiState`) and `path` for the dialog id.
### 5) Drag-and-drop/hover state (CanvasRenderer, ComponentTree)
**Bindings:** IDs and `dropPosition` stored in data; events mapped to custom actions for editor logic.
```json
{
"id": "canvas",
"type": "CanvasRenderer",
"bindings": {
"selectedId": { "source": "editor", "path": "selectedId" },
"hoveredId": { "source": "editor", "path": "hoveredId" },
"draggedOverId": { "source": "editor", "path": "draggedOverId" },
"dropPosition": { "source": "editor", "path": "dropPosition" }
},
"events": {
"onSelect": { "actions": [{ "id": "select-node", "type": "set-value", "target": "editor.selectedId", "expression": "event" }] },
"onHover": { "actions": [{ "id": "hover-node", "type": "set-value", "target": "editor.hoveredId", "expression": "event" }] },
"onHoverEnd": { "actions": [{ "id": "clear-hover", "type": "set-value", "target": "editor.hoveredId", "value": null }] },
"onDragOver": { "actions": [{ "id": "drag-over", "type": "custom", "params": { "targetId": "event" } }] },
"onDrop": { "actions": [{ "id": "drop-node", "type": "custom", "params": { "targetId": "event" } }] }
}
}
```
**Why:** drag/drop handlers need richer logic, so `custom` actions are the safest mapping until more JSON-native drag actions exist.
### 6) Data source CRUD (DataSourceManager/DataSourceCard)
**Bindings:** data sources array stored in JSON data; CRUD mapped to `create`/`update`/`delete` actions where possible.
```json
{
"id": "data-sources",
"type": "DataSourceManager",
"bindings": {
"dataSources": { "source": "schema", "path": "dataSources" }
},
"events": {
"onAdd": {
"actions": [
{
"id": "add-source",
"type": "create",
"target": "schema.dataSources",
"valueTemplate": {
"id": "Date.now()",
"type": "event.type",
"value": ""
}
}
]
},
"onEdit": {
"actions": [
{ "id": "open-source-editor", "type": "open-dialog", "target": "uiState", "path": "dataSourceEditor" }
]
},
"onDelete": {
"actions": [
{ "id": "delete-source", "type": "delete", "target": "schema.dataSources", "path": "id", "expression": "event" }
]
},
"onSave": {
"actions": [
{ "id": "update-source", "type": "update", "target": "schema.dataSources", "expression": "event" }
]
}
}
}
```
**Why:** CRUD aligns with the action schema (`create`, `update`, `delete`) and can use expressions/value templates to shape payloads.
## Prioritized binding additions (with example schemas)
1) **Dialog visibility + save/cancel actions** (CodeExplanationDialog, ComponentBindingDialog, DataSourceEditorDialog, TreeFormDialog)
- **Why priority:** unlocks core UI flows (open/close/save) and ties dialogs to JSON actions.
- **Example schema:** see “Dialog open/close control” above.
2) **Input value + change events** (SearchBar, SearchInput, TreeFormDialog)
- **Why priority:** essential for text filtering, search, and form editing in JSON-driven flows.
- **Example schema:** see “Input value + change events.”
3) **Selection and navigation events** (FileTabs, NavigationItem/Menu, TreeListPanel, TreeCard)
- **Why priority:** these are the primary navigation and selection surfaces in the editor UI.
- **Example schema:** see “List selection.”
4) **Toolbar/button action wiring** (SchemaEditorToolbar, ToolbarActions, EditorActions, ActionBar)
- **Why priority:** these buttons trigger important workflows (import/export, AI tools, preview).
- **Example schema:** see “Toolbar and button actions.”
5) **Drag-and-drop/hover orchestration** (CanvasRenderer, ComponentTree, ComponentPalette)
- **Why priority:** required for schema editing UI; may need `custom` actions for editor logic.
- **Example schema:** see “Drag-and-drop/hover state.”
6) **Data source CRUD flows** (DataSourceManager, DataSourceCard)
- **Why priority:** CRUD should map to built-in JSON actions to avoid bespoke handlers.
- **Example schema:** see “Data source CRUD.”

195
ROOT_CAUSE_ANALYSIS.md Normal file
View File

@@ -0,0 +1,195 @@
# Root Cause Analysis: JSON-Based React Component System
## Executive Summary
The repository is attempting to transition from a traditional TypeScript React component architecture to a JSON-based declarative UI system. The build is currently failing because the transition is incomplete - some TypeScript components were deleted but their imports remain, and the JSON component system cannot yet fully replace them.
## Current State: Hybrid System Failure
### What Was Attempted
1. **123 TypeScript components were deleted** (commit aa51074) and marked as "json-compatible" in the registry
2. **JSON component registry created** with 375+ component definitions
3. **JSON UI rendering system built** with component-renderer.tsx, expression evaluator, data binding, etc.
4. **Wrapper components created** for complex molecules that need special handling
### What's Broken
The build fails with these errors:
```
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "NavigationItem"
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "PageHeaderContent"
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "TreeCard"
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "TreeListHeader"
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "preloadMonacoEditor"
✘ [ERROR] No matching export in "src/components/molecules/index.ts" for import "LoadingFallback"
```
## Root Causes
### 1. **Incomplete Conversion Strategy**
Components were marked as JSON-compatible and deleted, but:
- The **consuming code still imports them as TypeScript modules**
- No migration was done to convert consumers to use the JSON renderer
- The JSON system exists but isn't wired into the main application flow
### 2. **Misunderstanding of JSON Component Architecture**
The JSON system is designed for **declarative page configurations**, not as a drop-in replacement for React components. Example:
**Traditional React:**
```tsx
import { TreeCard } from '@/components/molecules'
<TreeCard tree={data} onSelect={handleSelect} />
```
**JSON System:**
```json
{
"type": "TreeCard",
"bindings": {
"tree": { "source": "currentTree" }
},
"events": {
"onSelect": { "action": "selectTree" }
}
}
```
The JSON system requires:
- JSON configuration files
- JSONSchemaPageLoader or PageRenderer wrapper
- Data sources defined in JSON
- Event handlers defined in JSON
- Cannot be imported like a normal React component
### 3. **Deleted Components Still Referenced**
Components deleted but still imported:
- **TreeCard** - Used in TreeListPanel.tsx
- **TreeListHeader** - Used in TreeListPanel.tsx
- **LoadingFallback** - Used in JSONSchemaPageLoader.tsx and routes.tsx
- **NavigationItem** - File exists but not exported from index.ts
- **PageHeaderContent** - File exists but not exported from index.ts
- **preloadMonacoEditor** - Function exists but not exported from index.ts
### 4. **Module System vs Component Registry Mismatch**
The component-registry.ts uses `import.meta.glob` to load ALL .tsx files:
```ts
const moleculeModules = import.meta.glob('@/components/molecules/*.tsx', { eager: true })
```
This means:
- It CAN dynamically load TreeCard, TreeListHeader, etc. IF they exist as .tsx files
- But they were DELETED, so they can't be found
- The registry says they're "json-compatible" but provides no fallback
- The JSON renderer can use them IF loaded via JSON config, but direct imports fail
## The Fundamental Problem: No Working JSON System Examples
**Key Issue:** While the JSON UI infrastructure exists, there are NO working examples of pages that successfully:
1. Define a complex page entirely in JSON
2. Handle state management in JSON
3. Wire up all events in JSON
4. Replace an existing TypeScript page
The infrastructure exists but hasn't been proven to work end-to-end.
## Architecture Deep Dive
### JSON UI System Components
```
src/lib/json-ui/
├── component-renderer.tsx # Renders individual components from JSON
├── page-renderer.tsx # Renders full pages from JSON
├── component-registry.ts # Maps component names to React components
├── expression-evaluator.ts # Evaluates data binding expressions
├── hooks.ts # Data source hooks
├── schema.ts # TypeScript types
└── wrappers/ # Special wrappers for complex components
```
### How It Should Work (Theory)
1. Create JSON page definition in `src/config/ui-examples/my-page.json`
2. Load it with `<JSONSchemaPageLoader schemaPath="/config/ui-examples/my-page.json" />`
3. JSON renderer looks up components in registry
4. Registry loads them via import.meta.glob
5. Components render with data bindings and events
### Why It Doesn't Work (Reality)
1. **Deleted components can't be loaded** - glob can't find non-existent files
2. **Existing TypeScript pages import components directly** - they don't use JSON loader
3. **No migration path** - can't gradually convert pages
4. **Registry assumes all components exist as .tsx files** - no JSON-only components
## Two Possible Solutions
### Option A: Restore Components (Backward Compatibility)
**Goal:** Make the build work by restoring deleted components
Steps:
1. Restore TreeCard, TreeListHeader, LoadingFallback as .tsx files
2. Export NavigationItem, PageHeaderContent, preloadMonacoEditor
3. Keep JSON system for future use
4. Gradual migration when JSON system proven
**Pros:** Quick fix, maintains compatibility, low risk
**Cons:** Delays JSON transition, maintains technical debt
### Option B: Full JSON Transition (Forward-Looking)
**Goal:** Convert consuming pages to use JSON system
Steps:
1. Convert TreeListPanel.tsx to use JSON renderer
2. Convert routes.tsx to load JSON configs
3. Create JSON definitions for missing components
4. Delete rigid TypeScript components
5. Prove JSON system works end-to-end
**Pros:** Achieves goal of JSON system, modern architecture
**Cons:** High risk, requires extensive testing, may reveal more issues
## Recommendation
**Start with Option A**, then gradually move toward Option B:
1. **Immediate Fix** (Option A):
- Restore the 3 deleted components (TreeCard, TreeListHeader, LoadingFallback)
- Fix exports for existing components (NavigationItem, PageHeaderContent, preloadMonacoEditor)
- Get the build working
2. **Validation Phase**:
- Create 1-2 complete working examples of JSON pages
- Test all JSON system features (data binding, events, conditionals, loops)
- Document the conversion process
- Identify limitations
3. **Gradual Migration** (Option B):
- Convert simple pages first
- Build tooling to help convert TypeScript to JSON
- Only delete TypeScript after JSON proven working
- Keep wrappers for complex components
## Files Requiring Immediate Attention
1. `src/components/molecules/TreeCard.tsx` - RESTORE from aa51074~1
2. `src/components/molecules/TreeListHeader.tsx` - RESTORE from aa51074~1
3. `src/components/molecules/LoadingFallback.tsx` - RESTORE from aa51074~1
4. `src/components/molecules/index.ts` - ADD exports for NavigationItem, PageHeaderContent
5. `src/components/molecules/LazyMonacoEditor.tsx` - Already exports preloadMonacoEditor, just needs index.ts export
## Testing Plan
After fixes:
1. Run `npm run dev` - should start without errors
2. Run `npm run build` - should complete successfully
3. Run `npm run test:e2e` - should pass
4. Manually test pages that use restored components
5. Test JSON UI showcase page to verify JSON system still works
## Long-Term Vision Questions
1. Can complex state management work in JSON?
2. How do we handle TypeScript types and intellisense for JSON configs?
3. What about component composition and reusability?
4. Performance implications of JSON parsing and dynamic loading?
5. How do non-developers edit JSON configs safely?
6. Can we generate JSON from existing TypeScript components?
7. What's the migration path for 250+ existing pages?

1175
audit-report.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -236,6 +236,15 @@
"category": "showcase",
"description": "JSON UI system demonstration"
},
{
"name": "JSONConversionShowcase",
"path": "@/components/JSONConversionShowcase",
"export": "JSONConversionShowcase",
"type": "feature",
"preload": false,
"category": "showcase",
"description": "JSON conversion showcase overview"
},
{
"name": "SchemaEditor",
"path": "@/components/SchemaEditorPage",

141
delete-duplicates.sh Normal file
View File

@@ -0,0 +1,141 @@
rm "src\components\atoms\ActionButton.tsx"
rm "src\components\atoms\ActionCard.tsx"
rm "src\components\atoms\ActionIcon.tsx"
rm "src\components\atoms\Alert.tsx"
rm "src\components\atoms\AppLogo.tsx"
rm "src\components\atoms\Avatar.tsx"
rm "src\components\atoms\AvatarGroup.tsx"
rm "src\components\atoms\Badge.tsx"
rm "src\components\atoms\BindingIndicator.tsx"
rm "src\components\atoms\Breadcrumb.tsx"
rm "src\components\atoms\Button.tsx"
rm "src\components\atoms\ButtonGroup.tsx"
rm "src\components\atoms\Calendar.tsx"
rm "src\components\atoms\Card.tsx"
rm "src\components\atoms\Checkbox.tsx"
rm "src\components\atoms\Chip.tsx"
rm "src\components\atoms\CircularProgress.tsx"
rm "src\components\atoms\Code.tsx"
rm "src\components\atoms\ColorSwatch.tsx"
rm "src\components\atoms\CommandPalette.tsx"
rm "src\components\atoms\CompletionCard.tsx"
rm "src\components\atoms\ComponentPaletteItem.tsx"
rm "src\components\atoms\ComponentTreeNode.tsx"
rm "src\components\atoms\ConfirmButton.tsx"
rm "src\components\atoms\Container.tsx"
rm "src\components\atoms\ContextMenu.tsx"
rm "src\components\atoms\CountBadge.tsx"
rm "src\components\atoms\DataList.tsx"
rm "src\components\atoms\DataSourceBadge.tsx"
rm "src\components\atoms\DataTable.tsx"
rm "src\components\atoms\DatePicker.tsx"
rm "src\components\atoms\DetailRow.tsx"
rm "src\components\atoms\Divider.tsx"
rm "src\components\atoms\Dot.tsx"
rm "src\components\atoms\Drawer.tsx"
rm "src\components\atoms\EmptyMessage.tsx"
rm "src\components\atoms\EmptyState.tsx"
rm "src\components\atoms\EmptyStateIcon.tsx"
rm "src\components\atoms\ErrorBadge.tsx"
rm "src\components\atoms\FileIcon.tsx"
rm "src\components\atoms\Flex.tsx"
rm "src\components\atoms\Form.tsx"
rm "src\components\atoms\GlowCard.tsx"
rm "src\components\atoms\Grid.tsx"
rm "src\components\atoms\Heading.tsx"
rm "src\components\atoms\HelperText.tsx"
rm "src\components\atoms\HoverCard.tsx"
rm "src\components\atoms\IconButton.tsx"
rm "src\components\atoms\IconText.tsx"
rm "src\components\atoms\IconWrapper.tsx"
rm "src\components\atoms\InfoBox.tsx"
rm "src\components\atoms\InfoPanel.tsx"
rm "src\components\atoms\Input.tsx"
rm "src\components\atoms\Kbd.tsx"
rm "src\components\atoms\KeyValue.tsx"
rm "src\components\atoms\Label.tsx"
rm "src\components\atoms\Link.tsx"
rm "src\components\atoms\List.tsx"
rm "src\components\atoms\ListItem.tsx"
rm "src\components\atoms\LiveIndicator.tsx"
rm "src\components\atoms\LoadingSpinner.tsx"
rm "src\components\atoms\LoadingState.tsx"
rm "src\components\atoms\MetricCard.tsx"
rm "src\components\atoms\MetricDisplay.tsx"
rm "src\components\atoms\Modal.tsx"
rm "src\components\atoms\Notification.tsx"
rm "src\components\atoms\NumberInput.tsx"
rm "src\components\atoms\PageHeader.tsx"
rm "src\components\atoms\PanelHeader.tsx"
rm "src\components\atoms\ProgressBar.tsx"
rm "src\components\atoms\PropertyEditorField.tsx"
rm "src\components\atoms\Pulse.tsx"
rm "src\components\atoms\QuickActionButton.tsx"
rm "src\components\atoms\Radio.tsx"
rm "src\components\atoms\RangeSlider.tsx"
rm "src\components\atoms\Rating.tsx"
rm "src\components\atoms\ResponsiveGrid.tsx"
rm "src\components\atoms\ScrollArea.tsx"
rm "src\components\atoms\SearchInput.tsx"
rm "src\components\atoms\Section.tsx"
rm "src\components\atoms\SeedDataStatus.tsx"
rm "src\components\atoms\Select.tsx"
rm "src\components\atoms\Separator.tsx"
rm "src\components\atoms\Skeleton.tsx"
rm "src\components\atoms\Slider.tsx"
rm "src\components\atoms\Spacer.tsx"
rm "src\components\atoms\Sparkle.tsx"
rm "src\components\atoms\Spinner.tsx"
rm "src\components\atoms\Stack.tsx"
rm "src\components\atoms\StatCard.tsx"
rm "src\components\atoms\StatusBadge.tsx"
rm "src\components\atoms\StatusIcon.tsx"
rm "src\components\atoms\StepIndicator.tsx"
rm "src\components\atoms\Stepper.tsx"
rm "src\components\atoms\Switch.tsx"
rm "src\components\atoms\TabIcon.tsx"
rm "src\components\atoms\Table.tsx"
rm "src\components\atoms\Tabs.tsx"
rm "src\components\atoms\Tag.tsx"
rm "src\components\atoms\Text.tsx"
rm "src\components\atoms\TextArea.tsx"
rm "src\components\atoms\TextGradient.tsx"
rm "src\components\atoms\TextHighlight.tsx"
rm "src\components\atoms\Timeline.tsx"
rm "src\components\atoms\Timestamp.tsx"
rm "src\components\atoms\TipsCard.tsx"
rm "src\components\atoms\Toggle.tsx"
rm "src\components\atoms\Tooltip.tsx"
rm "src\components\atoms\TreeIcon.tsx"
rm "src\components\molecules\AppBranding.tsx"
rm "src\components\molecules\Breadcrumb.tsx"
rm "src\components\molecules\CanvasRenderer.tsx"
rm "src\components\molecules\CodeExplanationDialog.tsx"
rm "src\components\molecules\ComponentPalette.tsx"
rm "src\components\molecules\DataSourceCard.tsx"
rm "src\components\molecules\EditorActions.tsx"
rm "src\components\molecules\EditorToolbar.tsx"
rm "src\components\molecules\EmptyEditorState.tsx"
rm "src\components\molecules\FileTabs.tsx"
rm "src\components\molecules\LazyBarChart.tsx"
rm "src\components\molecules\LazyInlineMonacoEditor.tsx"
rm "src\components\molecules\LazyLineChart.tsx"
rm "src\components\molecules\LazyMonacoEditor.tsx"
rm "src\components\molecules\MonacoEditorPanel.tsx"
rm "src\components\molecules\NavigationGroupHeader.tsx"
rm "src\components\molecules\PropertyEditor.tsx"
rm "src\components\molecules\SearchBar.tsx"
rm "src\components\molecules\SearchInput.tsx"
rm "src\components\molecules\ToolbarButton.tsx"
rm "src\components\molecules\TreeFormDialog.tsx"
rm "src\components\organisms\AppHeader.tsx"
rm "src\components\organisms\EmptyCanvasState.tsx"
rm "src\components\organisms\PageHeader.tsx"
rm "src\components\organisms\SchemaCodeViewer.tsx"
rm "src\components\organisms\SchemaEditorCanvas.tsx"
rm "src\components\organisms\SchemaEditorLayout.tsx"
rm "src\components\organisms\SchemaEditorPropertiesPanel.tsx"
rm "src\components\organisms\SchemaEditorSidebar.tsx"
rm "src\components\organisms\SchemaEditorStatusBar.tsx"
rm "src\components\organisms\SchemaEditorToolbar.tsx"
rm "src\components\organisms\ToolbarActions.tsx"

View File

@@ -0,0 +1,262 @@
# Component Conversion Analysis
## Analysis of 68 React Components
After analyzing all 68 organism and molecule components, here's what can be converted to JSON:
### Categories
#### ✅ Fully Convertible to JSON (48 components)
These are presentational components with props, conditional rendering, and simple event handlers:
**Molecules (35):**
1. `LabelWithBadge` - ✅ Converted
2. `LoadingState` - ✅ Converted
3. `SaveIndicator` - ✅ Converted (computed sources replace hook)
4. `SearchInput` - ✅ Converted
5. `AppBranding` - Props + conditionals
6. `ActionBar` - Layout + buttons
7. `Breadcrumb` - ✅ Already converted
8. `DataCard` - ✅ Already converted
9. `EmptyState` - ✅ Already converted
10. `EmptyEditorState` - ✅ Already converted
11. `FileTabs` - ✅ Already converted
12. `NavigationGroupHeader` - Collapse trigger + state
13. `NavigationItem` - Button with active state
14. `PageHeaderContent` - Layout composition
15. `ToolbarButton` - Tooltip + IconButton
16. `TreeListHeader` - Buttons with events
17. `ComponentTreeEmptyState` - Config + icon lookup
18. `ComponentTreeHeader` - Counts + expand/collapse
19. `PropertyEditorEmptyState` - Config + icon lookup
20. `PropertyEditorHeader` - Title + count
21. `PropertyEditorSection` - Collapsible section
22. `DataSourceIdField` - Input with validation display
23. `KvSourceFields` - Form fields
24. `StaticSourceFields` - Form fields
25. `ComputedSourceFields` - Form fields
26. `GitHubBuildStatus` - Status display + polling
27. `LoadingFallback` - Spinner + message
28. `MonacoEditorPanel` - Layout wrapper (not editor itself)
29. `SearchBar` - SearchInput wrapper
30. `SeedDataManager` - Form + buttons (logic in parent)
31. `StorageSettings` - Form fields
32. `TreeCard` - Card + tree display
33. `TreeFormDialog` - Dialog with form (validation in parent)
34. `EditorActions` - Button group
35. `EditorToolbar` - Toolbar layout
**Organisms (13):**
1. `AppHeader` - ✅ Already converted
2. `EmptyCanvasState` - ✅ Already converted
3. `NavigationMenu` - ✅ Already converted
4. `PageHeader` - ✅ Already converted
5. `SchemaEditorLayout` - ✅ Already converted
6. `SchemaEditorSidebar` - ✅ Already converted
7. `SchemaEditorCanvas` - ✅ Already converted
8. `SchemaEditorPropertiesPanel` - ✅ Already converted
9. `SchemaEditorStatusBar` - Status display
10. `SchemaEditorToolbar` - Toolbar with actions
11. `ToolbarActions` - Action buttons
12. `SchemaCodeViewer` - Tabs + code display
13. `TreeListPanel` - List display
#### ⚠️ Needs Wrapper (Complex Hooks) (12 components)
These use hooks but the hook logic can be extracted to data sources or remain in a thin wrapper:
**Molecules (10):**
1. `BindingEditor` - Form with `useForm` hook → Extract to form state
2. `ComponentBindingDialog` - Dialog with `useForm` → Extract to form state
3. `DataSourceEditorDialog` - Complex form + validation → Wrapper + JSON form
4. `PropertyEditor` - Dynamic form generation → Computed source for fields
5. `ComponentPalette` - Search + filter → Computed source
6. `CanvasRenderer` - Recursive rendering → Could be JSON with loop support
7. `ComponentTree` - Tree state + drag/drop → State machine in JSON
8. `ComponentTreeNodes` - Recursive nodes → Loop construct
9. `CodeExplanationDialog` - Dialog + API call → Dialog JSON + API action
10. `DataSourceCard` - Card with actions + state → Separate state, JSON layout
**Organisms (2):**
1. `DataSourceManager` - Complex CRUD + hook → Extract `useDataSourceManager` logic
2. `JSONUIShowcase` - Examples display → Convert examples to JSON schema
#### ❌ Must Stay React (8 components)
These have imperative APIs, complex recursion, or third-party integration:
**Molecules (6):**
1. `LazyMonacoEditor` - Monaco integration (refs, imperative API)
2. `LazyInlineMonacoEditor` - Monaco integration
3. `MonacoEditorPanel` - Monaco wrapper
4. `LazyBarChart` - Recharts integration
5. `LazyLineChart` - Recharts integration
6. `LazyD3BarChart` - D3.js integration (imperative DOM manipulation)
**Organisms (2):**
1. `SchemaEditor` - Complex editor with drag-drop, undo/redo state machine
2. `DataBindingDesigner` - Visual flow editor with canvas manipulation
## Conversion Statistics
| Category | Count | Percentage |
|----------|-------|------------|
| ✅ Fully Convertible | 48 | 71% |
| ⚠️ Needs Wrapper | 12 | 18% |
| ❌ Must Stay React | 8 | 11% |
| **Total** | **68** | **100%** |
## Key Insights
### 1. Most Components Are Presentational
71% of components are pure presentation + simple logic that JSON can handle with:
- Data binding
- Computed sources
- Conditional rendering
- Event actions
- Loops (for lists)
### 2. Hooks Aren't a Blocker
Even components with hooks like `useSaveIndicator` can be converted:
- Time-based logic → Computed sources with polling
- Form state → Form data sources
- Local UI state → Page-level state
### 3. True Blockers
Only 8 components (11%) genuinely need React:
- Third-party library integrations (Monaco, D3, Recharts)
- Complex state machines (drag-drop, undo/redo)
- Imperative DOM manipulation
- Recursive algorithms (though loops might handle some)
### 4. Wrapper Pattern
The 12 "needs wrapper" components can have thin React wrappers that:
- Extract hooks to data source utilities
- Convert to JSON-configurable components
- Keep complex logic centralized
Example:
```tsx
// Thin wrapper
export function FormDialogWrapper({ schema, onSubmit }) {
const form = useForm()
return <JSONDialog schema={schema} formState={form} onSubmit={onSubmit} />
}
```
```json
// JSON configures it
{
"type": "FormDialogWrapper",
"props": {
"schema": { "$ref": "./schemas/user-form.json" }
}
}
```
## Recommended Conversion Priority
### Phase 1: Low-Hanging Fruit (35 molecules)
Convert all presentational molecules that are just composition:
- AppBranding, ActionBar, ToolbarButton, etc.
- **Impact**: Eliminate 51% of React components
### Phase 2: Organisms (13)
Convert layout organisms:
- TreeListPanel, SchemaCodeViewer, etc.
- **Impact**: Eliminate 70% of React components
### Phase 3: Extract Hooks (10 molecules)
Create data source utilities and convert:
- BindingEditor, ComponentPalette, etc.
- **Impact**: Eliminate 85% of React components
### Phase 4: Wrappers (2 organisms)
Create thin wrappers for complex components:
- DataSourceManager, JSONUIShowcase
- **Impact**: 89% conversion
### Final State
- **8 React components** (third-party integrations + complex editors)
- **60 JSON components** (89% of current React code)
- **100% JSON page definitions** (already achieved)
## Implementation Patterns
### Pattern 1: Simple Conversion
```tsx
// React
export function LabelWithBadge({ label, badge }) {
return (
<Flex>
<Text>{label}</Text>
{badge && <Badge>{badge}</Badge>}
</Flex>
)
}
```
```json
// JSON
{
"type": "div",
"className": "flex gap-2",
"children": [
{ "type": "Text", "dataBinding": { "children": { "source": "label" } } },
{
"type": "Badge",
"conditional": { "source": "badge", "operator": "truthy" },
"dataBinding": { "children": { "source": "badge" } }
}
]
}
```
### Pattern 2: Hook Extraction
```tsx
// React (before)
export function SaveIndicator({ lastSaved }) {
const { timeAgo, isRecent } = useSaveIndicator(lastSaved)
return <div>{isRecent ? 'Saved' : timeAgo}</div>
}
```
```json
// JSON (after) - hook logic → computed source
{
"dataSources": [
{
"id": "isRecent",
"type": "computed",
"compute": "(data) => Date.now() - data.lastSaved < 3000"
}
],
"type": "div",
"dataBinding": {
"children": {
"source": "isRecent",
"transform": "(isRecent, data) => isRecent ? 'Saved' : data.timeAgo"
}
}
}
```
### Pattern 3: Wrapper for Complex Logic
```tsx
// Thin React wrapper
export function DataSourceManagerWrapper(props) {
const manager = useDataSourceManager(props.dataSources)
return <JSONComponent schema={schema} data={manager} />
}
```
## Next Steps
1. ✅ Convert 35 simple molecules to JSON
2. ✅ Convert 13 layout organisms to JSON
3. ⚠️ Extract hooks to utilities for 10 components
4. ⚠️ Create wrappers for 2 complex organisms
5. ❌ Keep 8 third-party integrations as React
**Target: 60/68 components in JSON (89% conversion)**

471
docs/HYBRID_ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,471 @@
# Hybrid Architecture: JSON + React
## The Power of Both Worlds
This platform uses a **hybrid architecture** where JSON handles declarative UI composition while React provides the imperative implementation layer. This gives you the best of both worlds:
- **JSON** for structure, composition, and configuration
- **React** for complex logic, hooks, events, and interactivity
## What JSON Can't (and Shouldn't) Replace
### 1. Hooks
React hooks manage complex stateful logic that can't be represented declaratively:
```tsx
// ❌ Cannot be JSON
function useDataSourceManager(dataSources: DataSource[]) {
const [localSources, setLocalSources] = useState(dataSources)
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
useEffect(() => {
// Sync with external API
syncDataSources(localSources)
}, [localSources])
const getDependents = useCallback((id: string) => {
return localSources.filter(ds => ds.dependencies?.includes(id))
}, [localSources])
return { localSources, editingSource, getDependents, ... }
}
```
**Why React?** Hooks encapsulate complex imperative logic: side effects, memoization, refs, context. JSON is declarative and can't express these patterns.
### 2. Event Handlers with Complex Logic
Simple actions work in JSON, but complex event handling needs code:
```tsx
// ✅ Simple actions in JSON
{
"events": [{
"event": "onClick",
"actions": [
{ "type": "setState", "target": "count", "value": 1 },
{ "type": "toast", "title": "Clicked!" }
]
}]
}
// ❌ Complex logic needs React
function handleFileUpload(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
if (!file) return
// Validate file type
const validTypes = ['image/png', 'image/jpeg', 'image/svg+xml']
if (!validTypes.includes(file.type)) {
toast.error('Invalid file type')
return
}
// Check file size
const maxSize = 5 * 1024 * 1024 // 5MB
if (file.size > maxSize) {
toast.error('File too large')
return
}
// Convert to base64, compress, upload
compressImage(file).then(compressed => {
uploadToServer(compressed).then(url => {
updateState({ faviconUrl: url })
toast.success('Uploaded!')
})
})
}
```
**Why React?** Branching logic, async operations, error handling, file processing. JSON actions are linear and synchronous.
### 3. Classes and Interfaces
Type systems and OOP patterns require TypeScript:
```tsx
// ❌ Cannot be JSON
export interface DataSource {
id: string
type: DataSourceType
dependencies?: string[]
compute?: string
}
export class ThemeManager {
private themes: Map<string, Theme>
private listeners: Set<ThemeListener>
constructor(initialThemes: Theme[]) {
this.themes = new Map(initialThemes.map(t => [t.id, t]))
this.listeners = new Set()
}
applyTheme(themeId: string): void {
const theme = this.themes.get(themeId)
if (!theme) throw new Error(`Theme ${themeId} not found`)
// Apply CSS variables
Object.entries(theme.colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value)
})
// Notify listeners
this.listeners.forEach(listener => listener.onThemeChange(theme))
}
}
```
**Why React/TS?** Type safety, encapsulation, methods, private state. JSON is just data.
### 4. Complex Rendering Logic
Conditional rendering with complex business rules:
```tsx
// ❌ Cannot be JSON
function ComponentTree({ components }: ComponentTreeProps) {
const renderNode = (component: Component, depth: number): ReactNode => {
const hasChildren = component.children && component.children.length > 0
const isExpanded = expandedNodes.has(component.id)
const isDragging = draggedNode === component.id
const isDropTarget = dropTarget === component.id
// Determine visual state
const className = cn(
'tree-node',
{ 'tree-node--expanded': isExpanded },
{ 'tree-node--dragging': isDragging },
{ 'tree-node--drop-target': isDropTarget && canDrop(component) }
)
return (
<div
className={className}
style={{ paddingLeft: `${depth * 20}px` }}
onDragStart={() => handleDragStart(component)}
onDragOver={(e) => handleDragOver(e, component)}
onDrop={() => handleDrop(component)}
>
{/* Recursive rendering */}
{hasChildren && isExpanded && (
<div className="tree-children">
{component.children.map(child =>
renderNode(child, depth + 1)
)}
</div>
)}
</div>
)
}
return <div className="tree-root">{components.map(c => renderNode(c, 0))}</div>
}
```
**Why React?** Recursion, dynamic styling, drag-and-drop state, event coordination. JSON can't express recursive algorithms.
### 5. Third-Party Integrations
Libraries with imperative APIs need wrapper components:
```tsx
// ❌ Cannot be JSON
import MonacoEditor from '@monaco-editor/react'
export function LazyMonacoEditor({ value, onChange, language }: EditorProps) {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
const [isValid, setIsValid] = useState(true)
useEffect(() => {
// Configure Monaco
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
})
// Add custom validation
monaco.editor.onDidChangeMarkers(([uri]) => {
const markers = monaco.editor.getModelMarkers({ resource: uri })
setIsValid(markers.filter(m => m.severity === 8).length === 0)
})
}, [])
return (
<MonacoEditor
value={value}
onChange={onChange}
language={language}
onMount={(editor) => {
editorRef.current = editor
editor.addAction({
id: 'format-document',
label: 'Format Document',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
run: () => editor.getAction('editor.action.formatDocument')?.run()
})
}}
/>
)
}
```
**Why React?** Third-party libraries expect imperative APIs (refs, lifecycle methods). JSON can reference the wrapper, but can't create it.
## The Hybrid Pattern
### JSON References React Components
JSON schemas can reference any React component via the component registry:
```json
{
"id": "code-editor-section",
"type": "div",
"children": [
{
"id": "monaco-editor",
"type": "LazyMonacoEditor",
"props": {
"language": "typescript",
"theme": "vs-dark"
}
}
]
}
```
The `LazyMonacoEditor` is a React component with hooks, refs, and complex logic. JSON just *configures* it.
### Component Registry: The Bridge
```tsx
// src/lib/json-ui/component-registry.ts
export const componentRegistry: ComponentRegistry = {
// Simple components (could be JSON, but registered for convenience)
'Button': Button,
'Card': Card,
'Input': Input,
// Complex components (MUST be React)
'LazyMonacoEditor': LazyMonacoEditor,
'DataSourceManager': DataSourceManager,
'ComponentTree': ComponentTree,
'SchemaEditor': SchemaEditor,
// Hook-based components
'ProjectDashboard': ProjectDashboard, // uses multiple hooks
'CodeEditor': CodeEditor, // uses useEffect, useRef
'JSONModelDesigner': JSONModelDesigner, // uses custom hooks
}
```
### The 68 React Components
These aren't legacy cruft - they're **essential implementation**:
| Component Type | Count | Why React? |
|----------------|-------|------------|
| Hook-based managers | 15 | useState, useEffect, useCallback |
| Event-heavy UIs | 12 | Complex event handlers, drag-and-drop |
| Third-party wrappers | 8 | Monaco, Chart.js, D3 integrations |
| Recursive renderers | 6 | Tree views, nested structures |
| Complex forms | 10 | Validation, multi-step flows |
| Dialog/Modal managers | 8 | Portal rendering, focus management |
| Real-time features | 5 | WebSocket, polling, live updates |
| Lazy loaders | 4 | Code splitting, dynamic imports |
## When to Use What
### Use JSON When:
✅ Composing existing components
✅ Configuring layouts and styling
✅ Defining data sources and bindings
✅ Simple linear action chains
✅ Static page structure
✅ Theming and branding
✅ Feature flags and toggles
### Use React When:
✅ Complex state management (hooks)
✅ Imperative APIs (refs, third-party libs)
✅ Advanced event handling (validation, async)
✅ Recursive algorithms
✅ Performance optimization (memo, virtualization)
✅ Type-safe business logic (classes, interfaces)
✅ Side effects and lifecycle management
## Real-World Example: Data Source Manager
### What's in JSON
```json
{
"id": "data-source-section",
"type": "Card",
"children": [
{
"type": "CardHeader",
"children": [
{ "type": "CardTitle", "children": "Data Sources" }
]
},
{
"type": "CardContent",
"children": [
{
"id": "ds-manager",
"type": "DataSourceManager",
"dataBinding": {
"dataSources": { "source": "pageSources" }
},
"events": [{
"event": "onChange",
"actions": [
{ "type": "setState", "target": "pageSources", "valueFrom": "event" }
]
}]
}
]
}
]
}
```
**JSON handles:** Layout, composition, data binding, simple state updates
### What's in React
```tsx
// src/components/organisms/DataSourceManager.tsx
export function DataSourceManager({ dataSources, onChange }: Props) {
// ✅ Hook for complex state management
const {
dataSources: localSources,
addDataSource,
updateDataSource,
deleteDataSource,
getDependents, // ← Complex computed logic
} = useDataSourceManager(dataSources)
// ✅ Local UI state
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
const [dialogOpen, setDialogOpen] = useState(false)
// ✅ Complex event handler with validation
const handleDeleteSource = (id: string) => {
const dependents = getDependents(id)
if (dependents.length > 0) {
toast.error(`Cannot delete: ${dependents.length} sources depend on it`)
return
}
deleteDataSource(id)
onChange(localSources.filter(ds => ds.id !== id))
toast.success('Data source deleted')
}
// ✅ Conditional rendering based on complex state
const groupedSources = useMemo(() => ({
kv: localSources.filter(ds => ds.type === 'kv'),
computed: localSources.filter(ds => ds.type === 'computed'),
static: localSources.filter(ds => ds.type === 'static'),
}), [localSources])
return (
<div>
{localSources.length === 0 ? (
<EmptyState />
) : (
<Stack>
<DataSourceGroup sources={groupedSources.kv} />
<DataSourceGroup sources={groupedSources.static} />
<DataSourceGroup sources={groupedSources.computed} />
</Stack>
)}
<DataSourceEditorDialog
open={dialogOpen}
dataSource={editingSource}
onSave={handleSaveSource}
/>
</div>
)
}
```
**React handles:** Hooks, validation, dependency checking, grouping logic, dialog state
## The Power of Hybrid
### Flexibility
- **JSON**: Quick changes, visual editing, non-developer friendly
- **React**: Full programming power when needed
### Composition
- **JSON**: Compose pages from molecules and organisms
- **React**: Implement the organisms with complex logic
### Evolution
- **Start Simple**: Build in JSON, reference simple React components
- **Add Complexity**: When logic grows, extract to custom React component
- **Stay Declarative**: JSON schema stays clean, complexity hidden in components
### Example Evolution
**Day 1 - Pure JSON:**
```json
{
"type": "Button",
"events": [{ "event": "onClick", "actions": [{ "type": "toast" }] }]
}
```
**Day 30 - Need validation:**
```json
{
"type": "ValidatedButton", // ← Custom React component
"props": { "validationRules": ["required", "email"] }
}
```
```tsx
// Custom component when JSON isn't enough
function ValidatedButton({ validationRules, onClick, ...props }) {
const validate = useValidation(validationRules)
const handleClick = () => {
if (!validate()) {
toast.error('Validation failed')
return
}
onClick?.()
}
return <Button onClick={handleClick} {...props} />
}
```
**Day 90 - Complex workflow:**
```json
{
"type": "WorkflowButton", // ← Even more complex component
"props": { "workflowId": "user-onboarding" }
}
```
The JSON stays simple. The complexity lives in well-tested React components.
## Conclusion
The **68 React components aren't cruft** - they're the **essential implementation layer** that makes the JSON system powerful:
- **Hooks** manage complex state
- **Events** handle imperative interactions
- **Interfaces** provide type safety
- **Classes** encapsulate business logic
- **Third-party integrations** extend capabilities
JSON provides the **declarative structure**. React provides the **imperative power**.
Together, they create a system that's:
- **Easy** for simple cases (JSON)
- **Powerful** for complex cases (React)
- **Scalable** (add React components as needed)
- **Maintainable** (JSON is readable, React is testable)
This is the architecture of modern low-code platforms - not "no code," but **"right tool for the right job."**

View File

@@ -4,6 +4,8 @@
The JSON UI System is a declarative framework for building React user interfaces from JSON configurations. Instead of writing React components, you define your UI structure, data sources, and event handlers in JSON files, which are then rendered dynamically.
This document now serves as the consolidated reference for the JSON UI system. Legacy notes like `JSON_COMPONENTS.md`, `JSON_EXPRESSION_SYSTEM.md`, `JSON_COMPATIBILITY_IMPLEMENTATION.md`, the component usage report, and the old `json-components-list.json` artifact have been retired in favor of keeping the guidance in one place.
## Key Features
- **Fully Declarative**: Define complete UIs without writing React code

388
docs/JSON_ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,388 @@
# JSON-First Architecture
## Overview
This low-code platform uses a **JSON-first architecture** where the entire application is defined declaratively in JSON, eliminating React boilerplate and enabling visual editing, version control, and runtime customization.
## Core Principles
### 1. Everything is JSON
- **Pages**: All 35 application pages defined in JSON schemas
- **Components**: Atomic design library (atoms, molecules, organisms) in JSON
- **Themes**: Complete theming system configurable via JSON
- **Data**: State, bindings, and data sources declared in JSON
- **Actions**: Event handlers and side effects defined in JSON
### 2. Composition via $ref
JSON files reference each other using JSON Schema `$ref`:
```json
{
"id": "dashboard",
"components": [
{ "$ref": "./molecules/dashboard-header.json" },
{ "$ref": "./molecules/stats-grid.json" }
]
}
```
### 3. One Definition Per File
Following single-responsibility principle:
- 1 function per TypeScript file
- 1 type per TypeScript file
- 1 component definition per JSON file
- Compose larger structures via $ref
## Architecture Layers
```
┌─────────────────────────────────────┐
│ pages.json (35 pages) │ ← Router configuration
└──────────────┬──────────────────────┘
│ references
┌──────────────▼──────────────────────┐
│ Page Schemas (55 .json files) │ ← Page definitions
└──────────────┬──────────────────────┘
│ compose via $ref
┌──────────────▼──────────────────────┐
│ Organisms (8 .json files) │ ← Complex layouts
└──────────────┬──────────────────────┘
│ compose via $ref
┌──────────────▼──────────────────────┐
│ Molecules (23 .json files) │ ← Composed components
└──────────────┬──────────────────────┘
│ compose via $ref
┌──────────────▼──────────────────────┐
│ Atoms (23 .json files) │ ← Base components
└──────────────┬──────────────────────┘
│ reference
┌──────────────▼──────────────────────┐
│ React Components (68 .tsx) │ ← Implementation
│ Component Registry (100+ mapped) │
└─────────────────────────────────────┘
```
## File Structure
```
src/config/pages/
├── atoms/ # 23 base components
│ ├── button-primary.json
│ ├── heading-1.json
│ ├── text-muted.json
│ └── ...
├── molecules/ # 23 composed components
│ ├── dashboard-header.json
│ ├── stats-grid.json
│ ├── stat-card-base.json
│ └── ...
├── organisms/ # 8 complex layouts
│ ├── app-header.json
│ ├── navigation-menu.json
│ └── ...
├── layouts/ # Layout templates
│ └── single-column.json
├── data-sources/ # Data source templates
│ └── kv-storage.json
└── *.json # 55 page schemas
├── dashboard-simple.json
├── settings-page.json
└── ...
```
## JSON Schema Features
### Page Schema
```json
{
"$schema": "./schema/page-schema.json",
"id": "dashboard-simple",
"name": "Project Dashboard",
"description": "Overview of your project",
"icon": "ChartBar",
"layout": {
"$ref": "./layouts/single-column.json"
},
"dataSources": [
{
"id": "projectStats",
"$ref": "./data-sources/kv-storage.json",
"key": "project-stats",
"defaultValue": { "files": 0, "models": 0 }
}
],
"components": [
{ "$ref": "./molecules/dashboard-header.json" },
{ "$ref": "./molecules/stats-grid.json" }
]
}
```
### Data Binding
```json
{
"id": "files-value",
"type": "div",
"props": {
"className": "text-2xl font-bold",
"children": "0"
},
"dataBinding": {
"source": "projectStats",
"path": "files"
}
}
```
### Actions
```json
{
"type": "Button",
"events": [
{
"event": "onClick",
"actions": [
{
"type": "setState",
"target": "selectedTab",
"value": "colors"
},
{
"type": "toast",
"title": "Tab changed",
"variant": "success"
}
]
}
]
}
```
### Conditionals
```json
{
"type": "div",
"conditional": {
"source": "customColorCount",
"operator": "eq",
"value": 0
},
"children": [
{ "type": "p", "children": "No custom colors" }
]
}
```
## Theming System
### JSON Theme Definition
The entire theming system is JSON-based (theme.json):
```json
{
"sidebar": {
"width": "16rem",
"backgroundColor": "oklch(0.19 0.02 265)",
"foregroundColor": "oklch(0.95 0.01 265)"
},
"colors": {
"primary": "oklch(0.58 0.24 265)",
"accent": "oklch(0.75 0.20 145)",
"background": "oklch(0.15 0.02 265)"
},
"typography": {
"fontFamily": {
"body": "'IBM Plex Sans', sans-serif",
"heading": "'JetBrains Mono', monospace"
}
},
"spacing": {
"radius": "0.5rem"
}
}
```
### Runtime Theme Editing
Users can create theme variants and customize colors/fonts via JSON:
```json
{
"activeVariantId": "dark",
"variants": [
{
"id": "dark",
"name": "Dark Mode",
"colors": {
"primary": "#7c3aed",
"secondary": "#38bdf8",
"customColors": {
"success": "#10b981",
"warning": "#f59e0b"
}
}
}
]
}
```
## Data Sources
### KV Storage
```json
{
"id": "userData",
"type": "kv",
"key": "user-settings",
"defaultValue": { "theme": "dark" }
}
```
### Computed Sources
```json
{
"id": "totalFiles",
"type": "computed",
"compute": "(data) => data.files.length",
"dependencies": ["files"]
}
```
### Static Sources
```json
{
"id": "tabs",
"type": "static",
"defaultValue": ["colors", "typography", "preview"]
}
```
## Benefits Over Traditional React
### Traditional React Component (~50 lines)
```tsx
import { useState } from 'react'
import { Card } from '@/components/ui/card'
interface DashboardProps {
initialData?: { files: number }
}
export function Dashboard({ initialData }: DashboardProps) {
const [stats, setStats] = useState(initialData || { files: 0 })
return (
<div className="p-6">
<div className="border-b pb-4">
<h1 className="text-2xl font-bold">Dashboard</h1>
</div>
<Card className="p-6">
<div className="text-2xl font-bold">{stats.files}</div>
<div className="text-sm text-muted">Files</div>
</Card>
</div>
)
}
```
### JSON Equivalent (~15 lines)
```json
{
"id": "dashboard",
"dataSources": [
{ "id": "stats", "type": "kv", "key": "stats" }
],
"components": [
{ "$ref": "./molecules/dashboard-header.json" },
{
"$ref": "./molecules/stat-card.json",
"dataBinding": { "source": "stats", "path": "files" }
}
]
}
```
## Eliminated Boilerplate
**No imports** - Components referenced by type string
**No TypeScript interfaces** - Types inferred from registry
**No useState/useEffect** - State declared in dataSources
**No event handlers** - Actions declared in events array
**No prop drilling** - Data binding handles it
**No component exports** - Automatic via registry
**No JSX nesting** - Flat JSON structure with $ref
## Coverage Statistics
- **35/35 pages** use JSON schemas (100%)
- **0/35 pages** use React component references
- **109 JSON component files** created
- 23 atoms
- 23 molecules
- 8 organisms
- 55 page schemas
- **68 React components** remain as implementation layer
## Potential Cleanup Targets
### Deprecated Files (Safe to Remove)
- `src/config/default-pages.json` - Replaced by pages.json
- `src/config/json-demo.json` - Old demo file
- `src/config/template-ui.json` - Replaced by JSON schemas
### Keep (Still Used)
- `src/config/pages.json` - Active router configuration
- `theme.json` - Active theming system
- `src/config/feature-toggle-settings.json` - Feature flags
- All JSON schemas in `src/config/pages/`
## Best Practices
### 1. Atomic Granularity
Break components into smallest reusable units:
```
❌ dashboard.json (monolithic)
✅ dashboard-header.json + stats-grid.json + stat-card.json
```
### 2. $ref Composition
Always compose via references, never inline:
```json
{ "type": "div", "children": [ ... 50 lines ... ] }
{ "$ref": "./molecules/complex-section.json" }
```
### 3. Single Responsibility
One purpose per JSON file:
```
✅ stat-card-base.json (template)
✅ stat-card-files.json (specific instance)
✅ stat-card-models.json (specific instance)
```
### 4. Descriptive IDs
Use semantic IDs that describe purpose:
```json
{ "id": "dashboard-header" } // ✅ Good
{ "id": "div-1" } // ❌ Bad
```
## Future Enhancements
- [ ] Visual JSON editor for drag-and-drop page building
- [ ] Theme marketplace with sharable JSON themes
- [ ] Component library with searchable JSON snippets
- [ ] JSON validation and IntelliSense in VSCode
- [ ] Hot-reload JSON changes without app restart
- [ ] A/B testing via JSON variant switching
- [ ] Multi-tenant customization via tenant-specific JSONs
## Conclusion
This JSON-first architecture transforms React development from code-heavy to configuration-driven, enabling:
- **Visual editing** without touching code
- **Version control** friendly (JSON diffs)
- **Runtime customization** (load different JSONs)
- **Non-developer accessibility** (JSON is readable)
- **Rapid prototyping** (compose existing pieces)
- **Consistent patterns** (enforced by schema)
All without sacrificing the power of React when you need it - complex interactive components can still be written in React and referenced from JSON.

View File

@@ -0,0 +1,102 @@
# JSON Component Conversion Tasks
This task list captures the next steps for expanding JSON UI coverage, split between **component migrations** and **framework enablers**.
## Implementation Notes
- Component trees can live as JSON definitions.
- Custom behavior should be organized into hooks where appropriate.
- Types belong in `types` files; interfaces belong in dedicated `interfaces` files.
- Capture relevant conversion logs during work.
## Component Migration Tasks (Planned → Supported)
### Input Components
- [ ] **DatePicker**
- Add `DatePicker` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DatePicker` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **FileUpload**
- Add `FileUpload` to `ComponentType` in `src/types/json-ui.ts`.
- Register `FileUpload` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Display Components
- [ ] **CircularProgress**
- Add `CircularProgress` to `ComponentType` in `src/types/json-ui.ts`.
- Register `CircularProgress` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Divider**
- Add `Divider` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Divider` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **ProgressBar**
- Add `ProgressBar` to `ComponentType` in `src/types/json-ui.ts`.
- Register `ProgressBar` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Navigation Components
- [ ] **Breadcrumb**
- Decide whether JSON should map to `BreadcrumbNav` (atoms) or `Breadcrumb` (molecules).
- Align props and bindings to a single JSON-friendly surface.
- Register a single `Breadcrumb` entry and set status to `supported` in `json-components-registry.json`.
### Feedback Components
- [ ] **ErrorBadge**
- Add `ErrorBadge` to `ComponentType` in `src/types/json-ui.ts`.
- Register `ErrorBadge` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Notification**
- Add `Notification` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Notification` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **StatusIcon**
- Add `StatusIcon` to `ComponentType` in `src/types/json-ui.ts`.
- Register `StatusIcon` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
### Data Components
- [ ] **DataList**
- Add `DataList` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DataList` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **DataTable**
- Add `DataTable` to `ComponentType` in `src/types/json-ui.ts`.
- Register `DataTable` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **MetricCard**
- Add `MetricCard` to `ComponentType` in `src/types/json-ui.ts`.
- Register `MetricCard` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
- [ ] **Timeline**
- Add `Timeline` to `ComponentType` in `src/types/json-ui.ts`.
- Register `Timeline` in `src/lib/json-ui/component-registry.tsx`.
- Add metadata/defaults to `src/lib/component-definitions.ts`.
- Flip status to `supported` in `json-components-registry.json`.
## Framework Enablers
- [ ] **Event binding extensions**
- Expand event/action coverage to support richer interactions via JSON expressions.
- Confirm compatibility with existing `expression` and `valueTemplate` handling.
- [ ] **State binding system**
- Add support for stateful bindings needed by interactive components.
- Document and enforce which components require state binding.
- [ ] **JSON-friendly wrappers**
- Create wrapper components for hook-heavy/side-effect components.
- Register wrappers in the JSON registry instead of direct usage.
- [ ] **Registry normalization**
- Resolve duplicate component entries (e.g., multiple `Breadcrumb` variants) in `json-components-registry.json`.
- [ ] **Showcase schema coverage**
- Add JSON schema examples for each newly supported component to keep demos current.

107
docs/JSON_CONSTANTS.md Normal file
View File

@@ -0,0 +1,107 @@
# JSON Constants Migration Guide
## Overview
This document tracks the extraction of hardcoded constants from JSON component definitions into the centralized constants folder.
## Status
### ✅ Constants Folder Created
Location: `src/lib/json-ui/constants/`
Files:
- `sizes.ts` - Size-related constants
- `placements.ts` - Positioning constants
- `styles.ts` - CSS class constants
- `object-fit.ts` - Image object-fit constants
- `index.ts` - Centralized exports
### 📋 Constants Found in JSON Files
#### CopyButton (`copy-button.json`)
```javascript
// Line 11: sizeStyles
const sizeStyles = { sm: 'p-1', md: 'p-2', lg: 'p-3' }
// → BUTTON_SIZES
// Lines 25, 39: iconSize (duplicated)
const iconSize = { sm: 12, md: 16, lg: 20 }
// → ICON_SIZES
```
#### Popover (`popover.json`)
```javascript
// Line 39: placementStyles
const placementStyles = {
top: 'bottom-full mb-2 left-1/2 -translate-x-1/2',
bottom: 'top-full mt-2 left-1/2 -translate-x-1/2',
left: 'right-full mr-2 top-1/2 -translate-y-1/2',
right: 'left-full ml-2 top-1/2 -translate-y-1/2'
}
// → POPOVER_PLACEMENTS
```
#### Image (`image.json`)
```javascript
// Line 51: Dynamic object-fit (uses template literal)
return `${base} ${opacity} object-${fit}`
// Could use OBJECT_FIT_CLASSES but requires transform refactor
```
## Recommendations
### Option 1: Keep Inline (Current Approach)
**Pros:**
- No changes to component-renderer needed
- Self-contained JSON definitions
- Easy to understand transforms
**Cons:**
- Duplication of constants
- Harder to maintain consistency
- Magic strings scattered across files
### Option 2: Import Constants in Hooks
**Pros:**
- Hooks can use TypeScript constants
- No changes to JSON structure needed
- Immediate benefit for custom hooks
**Cons:**
- Only helps with hook-based logic
- Still have duplication in JSON transforms
### Option 3: Add Constants to Transform Context (Future)
**Pros:**
- Eliminates duplication entirely
- Type-safe constants usage
- Easier to update global styles
**Cons:**
- Requires component-renderer changes
- More complex transform evaluation
- Migration effort for existing JSON files
## Recommended Next Steps
1. **Short term:** Use constants in custom hooks (Option 2)
- Hooks can import from `@/lib/json-ui/constants`
- Reduce duplication in hook code
2. **Medium term:** Document best practices
- Add examples of using constants
- Create migration guide for new components
3. **Long term:** Enhanced transform context (Option 3)
- Update component-renderer to expose constants
- Migrate existing JSON files to use constants
- Remove inline const statements
## Files to Potentially Update
When migrating to Option 3:
- `copy-button.json` - sizeStyles, iconSize
- `popover.json` - placementStyles
- `menu.json` - May have similar patterns
- `file-upload.json` - May have size constants
- Any future components using similar patterns

View File

@@ -106,7 +106,12 @@ Converted three complex pages (Models, Component Trees, and Workflows) from trad
"type": "Component",
"bindings": { "prop": { "source": "...", "path": "..." } },
"events": [
{ "event": "click", "actions": [...] }
{
"event": "click",
"actions": [
{ "type": "set-value", "target": "selectedId", "expression": "event" }
]
}
]
}
]
@@ -115,6 +120,18 @@ Converted three complex pages (Models, Component Trees, and Workflows) from trad
}
```
### Action & Conditional Syntax
- Use supported JSON UI action types (for example, `set-value`, `toggle-value`, `show-toast`) with `target`, `path`, `value`, or `expression` fields instead of legacy `setState` actions.
- Replace legacy conditional objects (`{ "source": "...", "operator": "eq|gt|truthy|falsy", "value": ... }`) with `conditional.if` expressions:
```json
{
"conditional": {
"if": "modelCount === 0"
}
}
```
### Component Registry Integration
All JSON page wrappers are registered in `component-registry.ts`:
- `JSONModelDesigner`

View File

@@ -326,6 +326,241 @@ With transformations:
}
```
## Component Pattern Templates
Use these patterns as starting points when authoring JSON schemas. Each example includes
recommended prop shapes and binding strategies for predictable rendering and data flow.
### Form Pattern (Create/Edit)
**Recommended prop shape**
- `name`: field identifier used in data mappings.
- `label`: user-facing label.
- `placeholder`: optional hint text.
- `type`: input type (`text`, `email`, `number`, `date`, etc.).
- `required`: boolean for validation UI.
**Schema example**
```typescript
{
id: 'profile-form',
type: 'form',
props: {
className: 'space-y-4'
},
children: [
{
id: 'first-name',
type: 'Input',
props: {
name: 'firstName',
label: 'First name',
placeholder: 'Ada',
required: true
},
bindings: {
value: { source: 'formState', path: 'firstName' }
},
events: [
{
event: 'change',
actions: [
{
type: 'set-value',
target: 'formState',
path: 'firstName',
compute: (data, event) => event.target.value
}
]
}
]
},
{
id: 'email',
type: 'Input',
props: {
name: 'email',
label: 'Email',
placeholder: 'ada@lovelace.dev',
type: 'email'
},
bindings: {
value: { source: 'formState', path: 'email' }
},
events: [
{
event: 'change',
actions: [
{
type: 'set-value',
target: 'formState',
path: 'email',
compute: (data, event) => event.target.value
}
]
}
]
},
{
id: 'save-profile',
type: 'Button',
props: { children: 'Save profile' },
events: [
{
event: 'click',
actions: [
{
type: 'create',
target: 'profiles',
compute: (data) => ({
id: Date.now(),
...data.formState
})
},
{
type: 'set-value',
target: 'formState',
value: { firstName: '', email: '' }
}
]
}
]
}
]
}
```
**Recommended bindings**
- Use `bindings.value` for inputs and update a single `formState` data source.
- Use `set-value` with `path` to update individual fields and avoid cloning the whole object.
### Card Pattern (Summary/Stat)
**Recommended prop shape**
- `title`: primary label.
- `description`: supporting copy.
- `badge`: optional status tag.
- `icon`: optional leading icon name or component id.
**Schema example**
```typescript
{
id: 'stats-card',
type: 'Card',
props: { className: 'p-4' },
children: [
{
id: 'card-header',
type: 'div',
props: { className: 'flex items-center justify-between' },
children: [
{
id: 'card-title',
type: 'h3',
bindings: {
children: { source: 'stats', path: 'title' }
},
props: { className: 'text-lg font-semibold' }
},
{
id: 'card-badge',
type: 'Badge',
bindings: {
children: { source: 'stats', path: 'status' },
variant: {
source: 'stats',
path: 'status',
transform: (value) => (value === 'Active' ? 'success' : 'secondary')
}
}
}
]
},
{
id: 'card-description',
type: 'p',
props: { className: 'text-sm text-muted-foreground' },
bindings: {
children: { source: 'stats', path: 'description' }
}
}
]
}
```
**Recommended bindings**
- Bind the card text fields directly to a `stats` data source.
- Use `transform` for simple presentation mappings (status to badge variant).
### List Pattern (Collection + Row Actions)
**Recommended prop shape**
- `items`: array data source bound at the list container.
- `keyField`: unique field for list keys.
- `primary`: main text content (usually `name` or `title`).
- `secondary`: supporting text (optional).
- `actions`: array of action configs for row-level events.
**Schema example**
```typescript
{
id: 'task-list',
type: 'div',
bindings: {
children: {
source: 'tasks',
transform: (items) =>
items.map((item) => ({
id: `task-${item.id}`,
type: 'div',
props: { className: 'flex items-center justify-between py-2' },
children: [
{
id: `task-name-${item.id}`,
type: 'span',
bindings: {
children: { source: 'item', path: 'name' }
}
},
{
id: `task-toggle-${item.id}`,
type: 'Button',
props: { size: 'sm', variant: 'outline' },
bindings: {
children: {
source: 'item',
path: 'completed',
transform: (value) => (value ? 'Undo' : 'Complete')
}
},
events: [
{
event: 'click',
actions: [
{
type: 'update',
target: 'tasks',
id: item.id,
compute: (data) => ({
...item,
completed: !item.completed
})
}
]
}
]
}
]
}))
}
}
}
```
**Recommended bindings**
- Use a `transform` to map collection items into child component schemas.
- Use `{ source: 'item', path: 'field' }` when binding inside the item loop for clarity and efficiency.
## Event Handling
### Simple Event

View File

@@ -0,0 +1,9 @@
# JSON Components Tracker
| Component | Current Status | Blockers | Assignee |
| --- | --- | --- | --- |
| ActionCard | Supported | None | Unassigned |
| Breadcrumb | Planned | Needs JSON registry entry and schema examples | Unassigned |
| Notification | Planned | Requires JSON event bindings for dismiss/action | Unassigned |
| StatusIcon | Planned | Needs icon mapping strategy in JSON UI | Unassigned |
| CodeExplanationDialog | Maybe | Depends on JSON-safe dialog state handling | Unassigned |

1991
duplicate-analysis.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,10 @@ test.describe('CodeForge - Core Functionality', () => {
})
test('should load the application successfully', async ({ page }) => {
await expect(page.locator('body')).toBeVisible()
// Check root has children (content rendered)
await page.waitForSelector('#root > *', { timeout: 10000 })
const root = page.locator('#root')
await expect(root).toHaveCount(1)
})
test('should display main navigation', async ({ page }) => {
@@ -50,8 +53,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 expect(page.locator('body')).toBeVisible()
await page.waitForSelector('#root > *', { timeout: 10000 })
})
test('should work on tablet viewport', async ({ page }) => {
@@ -59,7 +62,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 expect(page.locator('body')).toBeVisible()
await page.waitForSelector('#root > *', { timeout: 10000 })
})
})

41
e2e/debug.spec.ts Normal file
View File

@@ -0,0 +1,41 @@
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,8 +4,12 @@ test.describe('CodeForge - Smoke Tests', () => {
test('app loads successfully', async ({ page }) => {
test.setTimeout(20000)
await page.goto('/', { waitUntil: 'networkidle', timeout: 15000 })
await expect(page.locator('body')).toBeVisible({ timeout: 5000 })
// 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 })
})
test('can navigate to dashboard tab', async ({ page }) => {

View File

@@ -0,0 +1,16 @@
import { test, expect } from '@playwright/test'
test.describe('visual regression', () => {
test('json conversion showcase', async ({ page }) => {
await page.goto('/json-conversion-showcase')
await page.waitForLoadState('networkidle')
await page.waitForFunction(() => {
const root = document.querySelector('#root')
return root && root.textContent && root.textContent.length > 0
})
await page.addStyleTag({
content: '* { transition: none !important; animation: none !important; }',
})
await expect(page).toHaveScreenshot('json-conversion-showcase.png', { fullPage: true })
})
})

13
fixtures/dev-qa/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Dev/QA Smoke Fixture Schemas
These JSON schemas provide lightweight smoke-test coverage for each JSON UI component category.
Each file is a standalone page schema that can be loaded in dev or QA to verify rendering.
## Categories
- `layout.json`
- `input.json`
- `display.json`
- `navigation.json`
- `feedback.json`
- `data.json`
- `custom.json`

View File

@@ -0,0 +1,40 @@
{
"id": "smoke-custom",
"name": "Smoke Custom",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "custom-section",
"type": "section",
"children": [
{
"id": "custom-heading",
"type": "Heading",
"props": {
"level": 3,
"children": "Custom Component Smoke Check"
}
},
{
"id": "custom-data-card",
"type": "DataCard",
"props": {
"title": "QA Metric",
"value": "99%",
"icon": "TrendUp"
}
},
{
"id": "custom-search",
"type": "SearchInput",
"props": {
"placeholder": "Search QA fixtures..."
}
}
]
}
]
}

46
fixtures/dev-qa/data.json Normal file
View File

@@ -0,0 +1,46 @@
{
"id": "smoke-data",
"name": "Smoke Data",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "data-section",
"type": "section",
"children": [
{
"id": "data-heading",
"type": "Heading",
"props": {
"level": 3,
"children": "Data Smoke Check"
}
},
{
"id": "data-list",
"type": "List",
"props": {
"items": ["QA record A", "QA record B", "QA record C"],
"emptyMessage": "No QA records"
}
},
{
"id": "data-table",
"type": "Table",
"props": {
"columns": [
{ "key": "name", "header": "Name" },
{ "key": "status", "header": "Status" }
],
"data": [
{ "name": "Smoke Run", "status": "Pass" },
{ "name": "Regression", "status": "Pending" }
]
}
}
]
}
]
}

View File

@@ -0,0 +1,42 @@
{
"id": "smoke-display",
"name": "Smoke Display",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "display-section",
"type": "section",
"children": [
{
"id": "display-heading",
"type": "Heading",
"props": {
"level": 3,
"children": "Display Smoke Check"
}
},
{
"id": "display-text",
"type": "Text",
"props": {
"children": "Checks text, badges, and separators for QA verification."
}
},
{
"id": "display-badge",
"type": "Badge",
"props": {
"children": "QA"
}
},
{
"id": "display-divider",
"type": "Divider"
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
{
"id": "smoke-feedback",
"name": "Smoke Feedback",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "feedback-section",
"type": "section",
"children": [
{
"id": "feedback-heading",
"type": "Heading",
"props": {
"level": 3,
"children": "Feedback Smoke Check"
}
},
{
"id": "feedback-alert",
"type": "Alert",
"props": {
"variant": "info",
"children": "QA info alert rendered."
}
},
{
"id": "feedback-status",
"type": "StatusBadge",
"props": {
"status": "active",
"children": "Active"
}
}
]
}
]
}

View File

@@ -0,0 +1,45 @@
{
"id": "smoke-input",
"name": "Smoke Input",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "input-section",
"type": "section",
"children": [
{
"id": "input-heading",
"type": "Heading",
"props": {
"level": 2,
"children": "Input Smoke Check"
}
},
{
"id": "input-control",
"type": "Input",
"props": {
"placeholder": "Enter QA value..."
}
},
{
"id": "input-toggle",
"type": "Switch",
"props": {
"checked": true
}
},
{
"id": "input-button",
"type": "Button",
"props": {
"children": "Submit"
}
}
]
}
]
}

View File

@@ -0,0 +1,67 @@
{
"id": "smoke-layout",
"name": "Smoke Layout",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "layout-container",
"type": "Container",
"props": {
"className": "py-6"
},
"children": [
{
"id": "layout-stack",
"type": "Stack",
"props": {
"gap": 4
},
"children": [
{
"id": "layout-card",
"type": "Card",
"children": [
{
"id": "layout-card-header",
"type": "CardHeader",
"children": [
{
"id": "layout-card-title",
"type": "CardTitle",
"props": {
"children": "Layout Smoke Check"
}
},
{
"id": "layout-card-description",
"type": "CardDescription",
"props": {
"children": "Ensures layout primitives render in QA."
}
}
]
},
{
"id": "layout-card-content",
"type": "CardContent",
"children": [
{
"id": "layout-card-text",
"type": "Text",
"props": {
"children": "This card is wrapped in Container and Stack components."
}
}
]
}
]
}
]
}
]
}
]
}

View File

@@ -0,0 +1,42 @@
{
"id": "smoke-navigation",
"name": "Smoke Navigation",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "navigation-section",
"type": "section",
"children": [
{
"id": "navigation-heading",
"type": "Heading",
"props": {
"level": 3,
"children": "Navigation Smoke Check"
}
},
{
"id": "navigation-link",
"type": "Link",
"props": {
"href": "/qa",
"children": "Go to QA overview"
}
},
{
"id": "navigation-breadcrumb",
"type": "Breadcrumb",
"props": {
"items": [
{ "label": "Home", "href": "/" },
{ "label": "QA" }
]
}
}
]
}
]
}

View File

@@ -1,494 +0,0 @@
[
{
"type": "div",
"name": "Container (div)",
"category": "layout",
"canHaveChildren": true,
"description": "Generic container element",
"status": "supported"
},
{
"type": "section",
"name": "Section",
"category": "layout",
"canHaveChildren": true,
"description": "Semantic section element",
"status": "supported"
},
{
"type": "article",
"name": "Article",
"category": "layout",
"canHaveChildren": true,
"description": "Semantic article element",
"status": "supported"
},
{
"type": "header",
"name": "Header",
"category": "layout",
"canHaveChildren": true,
"description": "Semantic header element",
"status": "supported"
},
{
"type": "footer",
"name": "Footer",
"category": "layout",
"canHaveChildren": true,
"description": "Semantic footer element",
"status": "supported"
},
{
"type": "main",
"name": "Main",
"category": "layout",
"canHaveChildren": true,
"description": "Semantic main content element",
"status": "supported"
},
{
"type": "Card",
"name": "Card",
"category": "layout",
"canHaveChildren": true,
"description": "Container card with optional header, content, and footer",
"status": "supported",
"subComponents": [
"CardHeader",
"CardTitle",
"CardDescription",
"CardContent",
"CardFooter"
]
},
{
"type": "Grid",
"name": "Grid",
"category": "layout",
"canHaveChildren": true,
"description": "Responsive grid layout",
"status": "supported"
},
{
"type": "Flex",
"name": "Flex",
"category": "layout",
"canHaveChildren": true,
"description": "Flexible box layout container",
"status": "supported"
},
{
"type": "Stack",
"name": "Stack",
"category": "layout",
"canHaveChildren": true,
"description": "Vertical or horizontal stack layout",
"status": "supported"
},
{
"type": "Container",
"name": "Container",
"category": "layout",
"canHaveChildren": true,
"description": "Centered container with max-width",
"status": "supported"
},
{
"type": "Button",
"name": "Button",
"category": "input",
"canHaveChildren": true,
"description": "Interactive button element",
"status": "supported"
},
{
"type": "Input",
"name": "Input",
"category": "input",
"canHaveChildren": false,
"description": "Text input field",
"status": "supported"
},
{
"type": "TextArea",
"name": "TextArea",
"category": "input",
"canHaveChildren": false,
"description": "Multi-line text input",
"status": "supported"
},
{
"type": "Select",
"name": "Select",
"category": "input",
"canHaveChildren": false,
"description": "Dropdown select control",
"status": "supported"
},
{
"type": "Checkbox",
"name": "Checkbox",
"category": "input",
"canHaveChildren": false,
"description": "Checkbox toggle control",
"status": "supported"
},
{
"type": "Radio",
"name": "Radio",
"category": "input",
"canHaveChildren": false,
"description": "Radio button selection",
"status": "supported"
},
{
"type": "Switch",
"name": "Switch",
"category": "input",
"canHaveChildren": false,
"description": "Toggle switch control",
"status": "supported"
},
{
"type": "Slider",
"name": "Slider",
"category": "input",
"canHaveChildren": false,
"description": "Numeric range slider",
"status": "supported"
},
{
"type": "NumberInput",
"name": "NumberInput",
"category": "input",
"canHaveChildren": false,
"description": "Numeric input with increment/decrement",
"status": "supported"
},
{
"type": "DatePicker",
"name": "DatePicker",
"category": "input",
"canHaveChildren": false,
"description": "Date selection input",
"status": "planned"
},
{
"type": "FileUpload",
"name": "FileUpload",
"category": "input",
"canHaveChildren": false,
"description": "File upload control",
"status": "planned"
},
{
"type": "Text",
"name": "Text",
"category": "display",
"canHaveChildren": true,
"description": "Text content with typography variants",
"status": "supported"
},
{
"type": "Heading",
"name": "Heading",
"category": "display",
"canHaveChildren": true,
"description": "Heading text with level (h1-h6)",
"status": "supported"
},
{
"type": "Label",
"name": "Label",
"category": "display",
"canHaveChildren": true,
"description": "Form label element",
"status": "supported"
},
{
"type": "Badge",
"name": "Badge",
"category": "display",
"canHaveChildren": true,
"description": "Small status or count indicator",
"status": "supported"
},
{
"type": "Tag",
"name": "Tag",
"category": "display",
"canHaveChildren": true,
"description": "Removable tag or chip",
"status": "supported"
},
{
"type": "Code",
"name": "Code",
"category": "display",
"canHaveChildren": true,
"description": "Inline or block code display",
"status": "supported"
},
{
"type": "Image",
"name": "Image",
"category": "display",
"canHaveChildren": false,
"description": "Image element with loading states",
"status": "supported"
},
{
"type": "Avatar",
"name": "Avatar",
"category": "display",
"canHaveChildren": false,
"description": "User avatar image",
"status": "supported"
},
{
"type": "Icon",
"name": "Icon",
"category": "display",
"canHaveChildren": false,
"description": "Icon from icon library",
"status": "planned"
},
{
"type": "Separator",
"name": "Separator",
"category": "display",
"canHaveChildren": false,
"description": "Visual divider line",
"status": "supported"
},
{
"type": "Divider",
"name": "Divider",
"category": "display",
"canHaveChildren": false,
"description": "Visual section divider",
"status": "planned"
},
{
"type": "Progress",
"name": "Progress",
"category": "display",
"canHaveChildren": false,
"description": "Progress bar indicator",
"status": "supported"
},
{
"type": "ProgressBar",
"name": "ProgressBar",
"category": "display",
"canHaveChildren": false,
"description": "Linear progress bar",
"status": "planned"
},
{
"type": "CircularProgress",
"name": "CircularProgress",
"category": "display",
"canHaveChildren": false,
"description": "Circular progress indicator",
"status": "planned"
},
{
"type": "Spinner",
"name": "Spinner",
"category": "display",
"canHaveChildren": false,
"description": "Loading spinner",
"status": "supported"
},
{
"type": "Skeleton",
"name": "Skeleton",
"category": "display",
"canHaveChildren": false,
"description": "Loading skeleton placeholder",
"status": "supported"
},
{
"type": "Link",
"name": "Link",
"category": "navigation",
"canHaveChildren": true,
"description": "Hyperlink element",
"status": "supported"
},
{
"type": "Breadcrumb",
"name": "Breadcrumb",
"category": "navigation",
"canHaveChildren": false,
"description": "Navigation breadcrumb trail",
"status": "planned"
},
{
"type": "Tabs",
"name": "Tabs",
"category": "navigation",
"canHaveChildren": true,
"description": "Tabbed interface container",
"status": "supported",
"subComponents": [
"TabsList",
"TabsTrigger",
"TabsContent"
]
},
{
"type": "Alert",
"name": "Alert",
"category": "feedback",
"canHaveChildren": true,
"description": "Alert notification message",
"status": "supported"
},
{
"type": "InfoBox",
"name": "InfoBox",
"category": "feedback",
"canHaveChildren": true,
"description": "Information box with icon",
"status": "supported"
},
{
"type": "Notification",
"name": "Notification",
"category": "feedback",
"canHaveChildren": true,
"description": "Toast notification",
"status": "planned"
},
{
"type": "StatusBadge",
"name": "StatusBadge",
"category": "feedback",
"canHaveChildren": false,
"description": "Status indicator badge",
"status": "supported"
},
{
"type": "StatusIcon",
"name": "StatusIcon",
"category": "feedback",
"canHaveChildren": false,
"description": "Status indicator icon",
"status": "planned"
},
{
"type": "EmptyState",
"name": "EmptyState",
"category": "feedback",
"canHaveChildren": true,
"description": "Empty state placeholder",
"status": "supported"
},
{
"type": "ErrorBadge",
"name": "ErrorBadge",
"category": "feedback",
"canHaveChildren": false,
"description": "Error state badge",
"status": "planned"
},
{
"type": "List",
"name": "List",
"category": "data",
"canHaveChildren": false,
"description": "Generic list renderer with custom items",
"status": "supported"
},
{
"type": "DataList",
"name": "DataList",
"category": "data",
"canHaveChildren": false,
"description": "Styled data list",
"status": "planned"
},
{
"type": "Table",
"name": "Table",
"category": "data",
"canHaveChildren": false,
"description": "Data table",
"status": "supported"
},
{
"type": "DataTable",
"name": "DataTable",
"category": "data",
"canHaveChildren": false,
"description": "Advanced data table with sorting and filtering",
"status": "planned"
},
{
"type": "KeyValue",
"name": "KeyValue",
"category": "data",
"canHaveChildren": false,
"description": "Key-value pair display",
"status": "supported"
},
{
"type": "Timeline",
"name": "Timeline",
"category": "data",
"canHaveChildren": false,
"description": "Timeline visualization",
"status": "planned"
},
{
"type": "StatCard",
"name": "StatCard",
"category": "data",
"canHaveChildren": false,
"description": "Statistic card display",
"status": "supported"
},
{
"type": "MetricCard",
"name": "MetricCard",
"category": "data",
"canHaveChildren": false,
"description": "Metric display card",
"status": "planned"
},
{
"type": "DataCard",
"name": "DataCard",
"category": "custom",
"canHaveChildren": false,
"description": "Custom data display card",
"status": "supported"
},
{
"type": "SearchInput",
"name": "SearchInput",
"category": "custom",
"canHaveChildren": false,
"description": "Search input with icon",
"status": "supported"
},
{
"type": "ActionBar",
"name": "ActionBar",
"category": "custom",
"canHaveChildren": false,
"description": "Action button toolbar",
"status": "supported"
},
{
"type": "Dialog",
"name": "Dialog",
"category": "layout",
"canHaveChildren": true,
"description": "Modal dialog overlay",
"status": "supported"
}
]

File diff suppressed because it is too large Load Diff

93
package-lock.json generated
View File

@@ -89,6 +89,8 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^17.0.0",
"tailwindcss": "^4.1.11",
"terser": "^5.46.0",
"tsx": "^4.21.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.38.0",
"vite": "^7.3.1"
@@ -823,9 +825,10 @@
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -4767,9 +4770,10 @@
},
"node_modules/buffer-from": {
"version": "1.1.2",
"license": "MIT",
"optional": true,
"peer": true
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/callsites": {
"version": "3.1.0",
@@ -5700,6 +5704,19 @@
"node": ">=6"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"dev": true,
@@ -6842,6 +6859,16 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"license": "Unlicense"
@@ -6965,9 +6992,10 @@
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"devOptional": true,
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -6981,9 +7009,10 @@
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -7051,9 +7080,10 @@
},
"node_modules/terser": {
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"devOptional": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -7069,9 +7099,10 @@
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"license": "MIT",
"optional": true,
"peer": true
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/three": {
"version": "0.175.0",
@@ -7117,6 +7148,40 @@
"version": "2.8.1",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
"license": "MIT",

View File

@@ -6,10 +6,12 @@
"scripts": {
"dev": "vite",
"kill": "fuser -k 5000/tcp",
"prebuild": "mkdir -p /tmp/dist || true",
"predev": "npm run components:generate-types",
"prebuild": "npm run components:generate-types && mkdir -p /tmp/dist || true",
"build": "tsc -b --noCheck && vite build",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"lint": "eslint . --fix && npm run lint:schemas",
"lint:check": "eslint . && npm run lint:schemas",
"lint:schemas": "node scripts/lint-json-ui-schemas.cjs",
"optimize": "vite optimize",
"preview": "vite preview --host 0.0.0.0 --port ${PORT:-80}",
"test:e2e": "playwright test",
@@ -21,8 +23,12 @@
"pages:list": "node scripts/list-pages.js",
"pages:validate": "tsx src/config/validate-config.ts",
"pages:generate": "node scripts/generate-page.js",
"schemas:validate": "tsx scripts/validate-json-schemas.ts",
"components:list": "node scripts/list-json-components.cjs",
"components:scan": "node scripts/scan-and-update-registry.cjs"
"components:generate-types": "tsx scripts/generate-json-ui-component-types.ts",
"components:scan": "node scripts/scan-and-update-registry.cjs",
"components:validate": "node scripts/validate-supported-components.cjs && tsx scripts/validate-json-registry.ts",
"audit:json": "tsx scripts/audit-json-components.ts"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
@@ -106,6 +112,8 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^17.0.0",
"tailwindcss": "^4.1.11",
"terser": "^5.46.0",
"tsx": "^4.21.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.38.0",
"vite": "^7.3.1"

View File

@@ -29,7 +29,7 @@ export default defineConfig({
webServer: {
command: 'npm run dev',
url: 'http://localhost:5000',
reuseExistingServer: !process.env.CI,
reuseExistingServer: true,
timeout: 120000,
stdout: 'pipe',
stderr: 'pipe',

View File

@@ -39,9 +39,13 @@
},
{
"id": "trends",
"type": "computed",
"compute": "(data) => ({ filesGrowth: 12, modelsGrowth: -3, componentsGrowth: 8, testsGrowth: 15 })",
"dependencies": ["metrics"]
"type": "static",
"defaultValue": {
"filesGrowth": 12,
"modelsGrowth": -3,
"componentsGrowth": 8,
"testsGrowth": 15
}
}
],
"components": [

View File

@@ -25,9 +25,12 @@
},
{
"id": "filteredFiles",
"type": "computed",
"compute": "(data) => {\n if (!data.searchQuery) return data.files;\n return data.files.filter(f => f.name.toLowerCase().includes(data.searchQuery.toLowerCase()));\n}",
"dependencies": ["files", "searchQuery"]
"type": "static",
"expression": "data.files",
"dependencies": [
"files",
"searchQuery"
]
}
],
"components": [

View File

@@ -0,0 +1,124 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "JSON Components Registry",
"type": "object",
"required": ["version", "description", "components"],
"properties": {
"$schema": {
"type": "string"
},
"version": {
"type": "string"
},
"description": {
"type": "string"
},
"lastUpdated": {
"type": "string"
},
"categories": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"sourceRoots": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"components": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"name",
"category",
"canHaveChildren",
"description",
"status",
"source"
],
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
},
"export": {
"type": "string"
},
"category": {
"type": "string"
},
"canHaveChildren": {
"type": "boolean"
},
"description": {
"type": "string"
},
"status": {
"type": "string"
},
"source": {
"type": "string",
"enum": ["atoms", "molecules", "organisms", "ui", "wrappers", "icons"]
},
"jsonCompatible": {
"type": "boolean"
},
"wrapperRequired": {
"type": "boolean"
},
"wrapperComponent": {
"type": "string"
},
"wrapperFor": {
"type": "string"
},
"load": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"export": {
"type": "string"
}
},
"required": ["export"],
"additionalProperties": false
},
"deprecated": {
"type": "object",
"properties": {
"replacedBy": {
"type": "string"
},
"message": {
"type": "string"
}
},
"additionalProperties": false
},
"metadata": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
}
},
"statistics": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": true
}

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env tsx
/**
* Analyze duplicate TSX files before deletion
* Check JSON contents to ensure they're complete
*/
import fs from 'fs'
import path from 'path'
import { globSync } from 'fs'
const ROOT_DIR = path.resolve(process.cwd())
const CONFIG_PAGES_DIR = path.join(ROOT_DIR, 'src/config/pages')
const COMPONENTS_DIR = path.join(ROOT_DIR, 'src/components')
const JSON_DEFS_DIR = path.join(ROOT_DIR, 'src/components/json-definitions')
function toKebabCase(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
}
interface AnalysisResult {
tsx: string
json: string
tsxSize: number
jsonSize: number
tsxHasHooks: boolean
tsxHasState: boolean
tsxHasEffects: boolean
jsonHasBindings: boolean
jsonHasChildren: boolean
recommendation: 'safe-to-delete' | 'needs-review' | 'keep-tsx'
reason: string
}
async function analyzeTsxFile(filePath: string): Promise<{
hasHooks: boolean
hasState: boolean
hasEffects: boolean
}> {
const content = fs.readFileSync(filePath, 'utf-8')
return {
hasHooks: /use[A-Z]/.test(content),
hasState: /useState|useReducer/.test(content),
hasEffects: /useEffect|useLayoutEffect/.test(content)
}
}
async function analyzeJsonFile(filePath: string): Promise<{
hasBindings: boolean
hasChildren: boolean
size: number
}> {
const content = fs.readFileSync(filePath, 'utf-8')
const json = JSON.parse(content)
return {
hasBindings: !!json.bindings || hasNestedBindings(json),
hasChildren: !!json.children,
size: content.length
}
}
function hasNestedBindings(obj: any): boolean {
if (!obj || typeof obj !== 'object') return false
if (obj.bindings) return true
for (const key in obj) {
if (hasNestedBindings(obj[key])) return true
}
return false
}
async function analyzeDuplicates() {
console.log('🔍 Analyzing duplicate TSX files...\n')
const results: AnalysisResult[] = []
// Find all TSX files in atoms, molecules, organisms
const categories = ['atoms', 'molecules', 'organisms']
for (const category of categories) {
const tsxFiles = globSync(path.join(COMPONENTS_DIR, category, '*.tsx'))
for (const tsxFile of tsxFiles) {
const basename = path.basename(tsxFile, '.tsx')
const kebab = toKebabCase(basename)
// Check for JSON equivalent in config/pages
const jsonPath = path.join(CONFIG_PAGES_DIR, category, `${kebab}.json`)
if (!fs.existsSync(jsonPath)) continue
// Check for JSON definition
const jsonDefPath = path.join(JSON_DEFS_DIR, `${kebab}.json`)
// Analyze both files
const tsxAnalysis = await analyzeTsxFile(tsxFile)
const tsxSize = fs.statSync(tsxFile).size
let jsonAnalysis = { hasBindings: false, hasChildren: false, size: 0 }
let actualJsonPath = jsonPath
if (fs.existsSync(jsonDefPath)) {
jsonAnalysis = await analyzeJsonFile(jsonDefPath)
actualJsonPath = jsonDefPath
} else if (fs.existsSync(jsonPath)) {
jsonAnalysis = await analyzeJsonFile(jsonPath)
}
// Determine recommendation
let recommendation: AnalysisResult['recommendation'] = 'safe-to-delete'
let reason = 'JSON definition exists'
if (tsxAnalysis.hasState || tsxAnalysis.hasEffects) {
if (!jsonAnalysis.hasBindings && jsonAnalysis.size < 500) {
recommendation = 'needs-review'
reason = 'TSX has state/effects but JSON seems incomplete'
} else {
recommendation = 'safe-to-delete'
reason = 'TSX has hooks but JSON should handle via createJsonComponentWithHooks'
}
}
if (tsxSize > 5000 && jsonAnalysis.size < 1000) {
recommendation = 'needs-review'
reason = 'TSX is large but JSON is small - might be missing content'
}
results.push({
tsx: path.relative(ROOT_DIR, tsxFile),
json: path.relative(ROOT_DIR, actualJsonPath),
tsxSize,
jsonSize: jsonAnalysis.size,
tsxHasHooks: tsxAnalysis.hasHooks,
tsxHasState: tsxAnalysis.hasState,
tsxHasEffects: tsxAnalysis.hasEffects,
jsonHasBindings: jsonAnalysis.hasBindings,
jsonHasChildren: jsonAnalysis.hasChildren,
recommendation,
reason
})
}
}
// Print results
console.log(`📊 Found ${results.length} duplicate components\n`)
const safeToDelete = results.filter(r => r.recommendation === 'safe-to-delete')
const needsReview = results.filter(r => r.recommendation === 'needs-review')
const keepTsx = results.filter(r => r.recommendation === 'keep-tsx')
console.log(`✅ Safe to delete: ${safeToDelete.length}`)
console.log(`⚠️ Needs review: ${needsReview.length}`)
console.log(`🔴 Keep TSX: ${keepTsx.length}\n`)
if (needsReview.length > 0) {
console.log('⚠️ NEEDS REVIEW:')
console.log('='.repeat(80))
for (const result of needsReview.slice(0, 10)) {
console.log(`\n${result.tsx}`)
console.log(`${result.json}`)
console.log(` TSX: ${result.tsxSize} bytes | JSON: ${result.jsonSize} bytes`)
console.log(` TSX hooks: ${result.tsxHasHooks} | state: ${result.tsxHasState} | effects: ${result.tsxHasEffects}`)
console.log(` JSON bindings: ${result.jsonHasBindings} | children: ${result.jsonHasChildren}`)
console.log(` Reason: ${result.reason}`)
}
if (needsReview.length > 10) {
console.log(`\n... and ${needsReview.length - 10} more`)
}
}
// Write full report
const reportPath = path.join(ROOT_DIR, 'duplicate-analysis.json')
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2))
console.log(`\n📄 Full report written to: ${reportPath}`)
// Generate deletion script for safe components
if (safeToDelete.length > 0) {
const deletionScript = safeToDelete.map(r => `rm "${r.tsx}"`).join('\n')
const scriptPath = path.join(ROOT_DIR, 'delete-duplicates.sh')
fs.writeFileSync(scriptPath, deletionScript)
console.log(`📝 Deletion script written to: ${scriptPath}`)
console.log(` Run: bash delete-duplicates.sh`)
}
}
analyzeDuplicates().catch(error => {
console.error('❌ Analysis failed:', error)
process.exit(1)
})

View File

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

@@ -0,0 +1,302 @@
#!/usr/bin/env tsx
/**
* Audit script for JSON component definitions
*
* Goals:
* 1. Phase out src/components TSX files
* 2. Audit existing JSON definitions for completeness and correctness
*/
import fs from 'fs'
import path from 'path'
import { globSync } from 'fs'
interface AuditIssue {
severity: 'error' | 'warning' | 'info'
category: string
file?: string
message: string
suggestion?: string
}
interface AuditReport {
timestamp: string
issues: AuditIssue[]
stats: {
totalJsonFiles: number
totalTsxFiles: number
registryEntries: number
orphanedJson: number
duplicates: number
obsoleteWrapperRefs: number
}
}
const ROOT_DIR = path.resolve(process.cwd())
const CONFIG_PAGES_DIR = path.join(ROOT_DIR, 'src/config/pages')
const COMPONENTS_DIR = path.join(ROOT_DIR, 'src/components')
const JSON_DEFS_DIR = path.join(ROOT_DIR, 'src/components/json-definitions')
const REGISTRY_FILE = path.join(ROOT_DIR, 'json-components-registry.json')
async function loadRegistry(): Promise<any> {
const content = fs.readFileSync(REGISTRY_FILE, 'utf-8')
return JSON.parse(content)
}
function findAllFiles(pattern: string, cwd: string = ROOT_DIR): string[] {
const fullPattern = path.join(cwd, pattern)
return globSync(fullPattern, { ignore: '**/node_modules/**' })
}
function toKebabCase(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
}
function toPascalCase(str: string): string {
return str
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('')
}
async function auditJsonComponents(): Promise<AuditReport> {
const issues: AuditIssue[] = []
const registry = await loadRegistry()
// Find all files
const jsonFiles = findAllFiles('src/config/pages/**/*.json')
const tsxFiles = findAllFiles('src/components/**/*.tsx')
const jsonDefFiles = findAllFiles('src/components/json-definitions/*.json')
console.log(`📊 Found ${jsonFiles.length} JSON files in config/pages`)
console.log(`📊 Found ${tsxFiles.length} TSX files in src/components`)
console.log(`📊 Found ${jsonDefFiles.length} JSON definitions`)
console.log(`📊 Found ${registry.components?.length || 0} registry entries\n`)
// Build registry lookup maps
const registryByType = new Map<string, any>()
const registryByName = new Map<string, any>()
if (registry.components) {
for (const component of registry.components) {
if (component.type) registryByType.set(component.type, component)
if (component.name) registryByName.set(component.name, component)
}
}
// Check 1: Find TSX files that have JSON equivalents in config/pages
console.log('🔍 Checking for TSX files that could be replaced with JSON...')
const tsxBasenames = new Set<string>()
for (const tsxFile of tsxFiles) {
const basename = path.basename(tsxFile, '.tsx')
const dir = path.dirname(tsxFile)
const category = path.basename(dir) // atoms, molecules, organisms
if (!['atoms', 'molecules', 'organisms'].includes(category)) continue
tsxBasenames.add(basename)
const kebab = toKebabCase(basename)
// Check if there's a corresponding JSON file in config/pages
const possibleJsonPath = path.join(CONFIG_PAGES_DIR, category, `${kebab}.json`)
if (fs.existsSync(possibleJsonPath)) {
issues.push({
severity: 'warning',
category: 'duplicate-implementation',
file: tsxFile,
message: `TSX file has JSON equivalent at ${path.relative(ROOT_DIR, possibleJsonPath)}`,
suggestion: `Consider removing TSX and routing through JSON renderer`
})
}
}
// Check 2: Find JSON files without registry entries
console.log('🔍 Checking for orphaned JSON files...')
for (const jsonFile of jsonFiles) {
const content = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'))
const componentType = content.type
if (componentType && !registryByType.has(componentType)) {
issues.push({
severity: 'error',
category: 'orphaned-json',
file: jsonFile,
message: `JSON file references type "${componentType}" which is not in registry`,
suggestion: `Add registry entry for ${componentType} in json-components-registry.json`
})
}
}
// Check 3: Find components with obsolete wrapper references
console.log('🔍 Checking for obsolete wrapper references...')
for (const component of registry.components || []) {
if (component.wrapperRequired || component.wrapperComponent) {
issues.push({
severity: 'warning',
category: 'obsolete-wrapper-ref',
file: `registry: ${component.type}`,
message: `Component "${component.type}" has obsolete wrapperRequired/wrapperComponent fields`,
suggestion: `Remove wrapperRequired and wrapperComponent fields - use createJsonComponentWithHooks instead`
})
}
}
// Check 4: Find components with load.path that don't exist
console.log('🔍 Checking for broken load paths...')
for (const component of registry.components || []) {
if (component.load?.path) {
const loadPath = component.load.path.replace('@/', 'src/')
const possibleExtensions = ['.tsx', '.ts', '.jsx', '.js']
let found = false
for (const ext of possibleExtensions) {
if (fs.existsSync(path.join(ROOT_DIR, loadPath + ext))) {
found = true
break
}
}
if (!found) {
issues.push({
severity: 'error',
category: 'broken-load-path',
file: `registry: ${component.type}`,
message: `Component "${component.type}" has load.path "${component.load.path}" but file not found`,
suggestion: `Fix or remove load.path in registry`
})
}
}
}
// Check 5: Components in src/components/molecules without JSON definitions
console.log('🔍 Checking molecules without JSON definitions...')
const moleculeTsxFiles = tsxFiles.filter(f => f.includes('/molecules/'))
const jsonDefBasenames = new Set(
jsonDefFiles.map(f => path.basename(f, '.json'))
)
for (const tsxFile of moleculeTsxFiles) {
const basename = path.basename(tsxFile, '.tsx')
const kebab = toKebabCase(basename)
if (!jsonDefBasenames.has(kebab) && registryByType.has(basename)) {
const entry = registryByType.get(basename)
if (entry.source === 'molecules' && !entry.load?.path) {
issues.push({
severity: 'info',
category: 'potential-conversion',
file: tsxFile,
message: `Molecule "${basename}" could potentially be converted to JSON`,
suggestion: `Evaluate if ${basename} can be expressed as pure JSON`
})
}
}
}
const stats = {
totalJsonFiles: jsonFiles.length,
totalTsxFiles: tsxFiles.length,
registryEntries: registry.components?.length || 0,
orphanedJson: issues.filter(i => i.category === 'orphaned-json').length,
duplicates: issues.filter(i => i.category === 'duplicate-implementation').length,
obsoleteWrapperRefs: issues.filter(i => i.category === 'obsolete-wrapper-ref').length
}
return {
timestamp: new Date().toISOString(),
issues,
stats
}
}
function printReport(report: AuditReport) {
console.log('\n' + '='.repeat(80))
console.log('📋 AUDIT REPORT')
console.log('='.repeat(80))
console.log(`\n📅 Generated: ${report.timestamp}\n`)
console.log('📈 Statistics:')
console.log(` • Total JSON files: ${report.stats.totalJsonFiles}`)
console.log(` • Total TSX files: ${report.stats.totalTsxFiles}`)
console.log(` • Registry entries: ${report.stats.registryEntries}`)
console.log(` • Orphaned JSON: ${report.stats.orphanedJson}`)
console.log(` • Obsolete wrapper refs: ${report.stats.obsoleteWrapperRefs}`)
console.log(` • Duplicate implementations: ${report.stats.duplicates}\n`)
// Group issues by category
const byCategory = new Map<string, AuditIssue[]>()
for (const issue of report.issues) {
if (!byCategory.has(issue.category)) {
byCategory.set(issue.category, [])
}
byCategory.get(issue.category)!.push(issue)
}
// Print issues by severity
const severityOrder = ['error', 'warning', 'info'] as const
const severityIcons = { error: '❌', warning: '⚠️', info: '' }
for (const severity of severityOrder) {
const issuesOfSeverity = report.issues.filter(i => i.severity === severity)
if (issuesOfSeverity.length === 0) continue
console.log(`\n${severityIcons[severity]} ${severity.toUpperCase()} (${issuesOfSeverity.length})`)
console.log('-'.repeat(80))
const categories = new Map<string, AuditIssue[]>()
for (const issue of issuesOfSeverity) {
if (!categories.has(issue.category)) {
categories.set(issue.category, [])
}
categories.get(issue.category)!.push(issue)
}
for (const [category, issues] of categories) {
console.log(`\n ${category.replace(/-/g, ' ').toUpperCase()} (${issues.length}):`)
for (const issue of issues.slice(0, 5)) { // Show first 5 of each category
console.log(`${issue.file || 'N/A'}`)
console.log(` ${issue.message}`)
if (issue.suggestion) {
console.log(` 💡 ${issue.suggestion}`)
}
}
if (issues.length > 5) {
console.log(` ... and ${issues.length - 5} more`)
}
}
}
console.log('\n' + '='.repeat(80))
console.log(`Total issues found: ${report.issues.length}`)
console.log('='.repeat(80) + '\n')
}
async function main() {
console.log('🔍 Starting JSON component audit...\n')
const report = await auditJsonComponents()
printReport(report)
// Write report to file
const reportPath = path.join(ROOT_DIR, 'audit-report.json')
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
console.log(`📄 Full report written to: ${reportPath}\n`)
// Exit with error code if there are errors
const errorCount = report.issues.filter(i => i.severity === 'error').length
if (errorCount > 0) {
console.log(`❌ Audit failed with ${errorCount} errors`)
process.exit(1)
} else {
console.log('✅ Audit completed successfully')
}
}
main().catch(error => {
console.error('❌ Audit failed:', error)
process.exit(1)
})

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env tsx
/**
* Cleanup script to remove obsolete wrapper references from registry
*/
import fs from 'fs'
import path from 'path'
const REGISTRY_FILE = path.resolve(process.cwd(), 'json-components-registry.json')
async function cleanupRegistry() {
console.log('🧹 Cleaning up registry...\n')
// Read registry
const content = fs.readFileSync(REGISTRY_FILE, 'utf-8')
const registry = JSON.parse(content)
let cleanedCount = 0
const cleanedComponents: string[] = []
// Remove obsolete fields from all components
if (registry.components) {
for (const component of registry.components) {
let modified = false
if (component.wrapperRequired !== undefined) {
delete component.wrapperRequired
modified = true
}
if (component.wrapperComponent !== undefined) {
delete component.wrapperComponent
modified = true
}
if (modified) {
cleanedCount++
cleanedComponents.push(component.type || component.name || 'Unknown')
}
}
}
// Write back to file with proper formatting
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2) + '\n')
console.log(`✅ Cleaned ${cleanedCount} components\n`)
if (cleanedComponents.length > 0) {
console.log('📋 Cleaned components:')
cleanedComponents.slice(0, 10).forEach(name => {
console.log(`${name}`)
})
if (cleanedComponents.length > 10) {
console.log(` ... and ${cleanedComponents.length - 10} more`)
}
}
console.log('\n✨ Registry cleanup complete!')
}
cleanupRegistry().catch(error => {
console.error('❌ Cleanup failed:', error)
process.exit(1)
})

View File

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

View File

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

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

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

@@ -0,0 +1,41 @@
#!/usr/bin/env tsx
/**
* Fix index.ts files to only export existing TSX files
*/
import fs from 'fs'
import path from 'path'
import { globSync } from 'fs'
const ROOT_DIR = path.resolve(process.cwd())
const COMPONENTS_DIR = path.join(ROOT_DIR, 'src/components')
const categories = ['atoms', 'molecules', 'organisms']
for (const category of categories) {
const categoryDir = path.join(COMPONENTS_DIR, category)
const indexPath = path.join(categoryDir, 'index.ts')
if (!fs.existsSync(indexPath)) continue
// Find all TSX files in this category
const tsxFiles = globSync(path.join(categoryDir, '*.tsx'))
const basenames = tsxFiles.map(f => path.basename(f, '.tsx'))
console.log(`\n📁 ${category}/`)
console.log(` Found ${basenames.length} TSX files`)
// Generate new exports
const exports = basenames
.sort()
.map(name => `export { ${name} } from './${name}'`)
.join('\n')
// Write new index file
const content = `// Auto-generated - only exports existing TSX files\n${exports}\n`
fs.writeFileSync(indexPath, content)
console.log(` ✅ Updated ${category}/index.ts`)
}
console.log('\n✨ All index files updated!')

View File

@@ -0,0 +1,50 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
interface RegistryComponent {
type?: string
name?: string
export?: string
}
interface RegistryData {
components?: RegistryComponent[]
}
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(__dirname, '..')
const registryPath = path.join(rootDir, 'json-components-registry.json')
const outputPath = path.join(rootDir, 'src/types/json-ui-component-types.ts')
const registryData = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as RegistryData
const components = registryData.components ?? []
const seen = new Set<string>()
const componentTypes = components.flatMap((component) => {
const typeName = component.type ?? component.name ?? component.export
if (!typeName || typeof typeName !== 'string') {
throw new Error('Registry component is missing a valid type/name/export entry.')
}
if (seen.has(typeName)) {
return []
}
seen.add(typeName)
return [typeName]
})
const lines = [
'// This file is auto-generated by scripts/generate-json-ui-component-types.ts.',
'// Do not edit this file directly.',
'',
'export const jsonUIComponentTypes = [',
...componentTypes.map((typeName) => ` ${JSON.stringify(typeName)},`),
'] as const',
'',
'export type JSONUIComponentType = typeof jsonUIComponentTypes[number]',
'',
]
fs.writeFileSync(outputPath, `${lines.join('\n')}`)
console.log(`✅ Wrote ${componentTypes.length} component types to ${outputPath}`)

View File

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

@@ -0,0 +1,252 @@
const fs = require('fs')
const path = require('path')
const rootDir = path.resolve(__dirname, '..')
const definitionsPath = path.join(rootDir, 'src', 'lib', 'component-definitions.json')
const schemaDirs = [
path.join(rootDir, 'src', 'schemas'),
path.join(rootDir, 'public', 'schemas'),
]
const commonProps = new Set(['className', 'style', 'children'])
const bindingSourceTypes = new Set(['data', 'bindings', 'state'])
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
const fileExists = (filePath) => fs.existsSync(filePath)
const componentDefinitions = readJson(definitionsPath)
const definitionsByType = new Map(
componentDefinitions
.filter((definition) => definition.type)
.map((definition) => [definition.type, definition])
)
const errors = []
const reportError = (file, pathLabel, message) => {
errors.push({ file, path: pathLabel, message })
}
const collectSchemaFiles = (dirs) => {
const files = []
dirs.forEach((dir) => {
if (!fileExists(dir)) return
fs.readdirSync(dir).forEach((entry) => {
if (!entry.endsWith('.json')) return
files.push(path.join(dir, entry))
})
})
return files
}
const isPageSchema = (schema) =>
schema
&& typeof schema === 'object'
&& schema.layout
&& Array.isArray(schema.components)
const extractSchemas = (data, filePath) => {
if (isPageSchema(data)) {
return [{ name: filePath, schema: data }]
}
if (data && typeof data === 'object') {
const schemas = Object.entries(data)
.filter(([, value]) => isPageSchema(value))
.map(([key, value]) => ({ name: `${filePath}:${key}`, schema: value }))
if (schemas.length > 0) {
return schemas
}
}
return []
}
const validateBindings = (bindings, fileLabel, pathLabel, contextVars, dataSourceIds, definition) => {
if (!bindings) return
const propDefinitions = definition?.props
? new Map(definition.props.map((prop) => [prop.name, prop]))
: null
Object.entries(bindings).forEach(([propName, binding]) => {
if (propDefinitions) {
if (!propDefinitions.has(propName) && !commonProps.has(propName)) {
reportError(fileLabel, `${pathLabel}.bindings.${propName}`, `Invalid binding for unknown prop "${propName}"`)
return
}
const propDefinition = propDefinitions.get(propName)
if (propDefinition && propDefinition.supportsBinding !== true) {
reportError(fileLabel, `${pathLabel}.bindings.${propName}`, `Binding not supported for prop "${propName}"`)
}
}
if (binding && typeof binding === 'object') {
const sourceType = binding.sourceType ?? 'data'
if (!bindingSourceTypes.has(sourceType)) {
reportError(
fileLabel,
`${pathLabel}.bindings.${propName}.sourceType`,
`Unsupported binding sourceType "${sourceType}"`
)
}
const source = binding.source
if (source && sourceType !== 'state') {
const isKnownSource = dataSourceIds.has(source) || contextVars.has(source)
if (!isKnownSource) {
reportError(
fileLabel,
`${pathLabel}.bindings.${propName}.source`,
`Binding source "${source}" is not defined in dataSources or loop context`
)
}
}
}
})
}
const validateDataBinding = (dataBinding, fileLabel, pathLabel, contextVars, dataSourceIds) => {
if (!dataBinding || typeof dataBinding !== 'object') return
const sourceType = dataBinding.sourceType ?? 'data'
if (!bindingSourceTypes.has(sourceType)) {
reportError(
fileLabel,
`${pathLabel}.dataBinding.sourceType`,
`Unsupported dataBinding sourceType "${sourceType}"`
)
}
if (dataBinding.source && sourceType !== 'state') {
const isKnownSource = dataSourceIds.has(dataBinding.source) || contextVars.has(dataBinding.source)
if (!isKnownSource) {
reportError(
fileLabel,
`${pathLabel}.dataBinding.source`,
`Data binding source "${dataBinding.source}" is not defined in dataSources or loop context`
)
}
}
}
const validateRequiredProps = (component, fileLabel, pathLabel, definition, bindings) => {
if (!definition?.props) return
definition.props.forEach((prop) => {
if (!prop.required) return
const hasProp = component.props && Object.prototype.hasOwnProperty.call(component.props, prop.name)
const hasBinding = bindings && Object.prototype.hasOwnProperty.call(bindings, prop.name)
if (!hasProp && (!prop.supportsBinding || !hasBinding)) {
reportError(
fileLabel,
`${pathLabel}.props.${prop.name}`,
`Missing required prop "${prop.name}" for component type "${component.type}"`
)
}
})
}
const validateProps = (component, fileLabel, pathLabel, definition) => {
if (!component.props || !definition?.props) return
const allowedProps = new Set(definition.props.map((prop) => prop.name))
commonProps.forEach((prop) => allowedProps.add(prop))
Object.keys(component.props).forEach((propName) => {
if (!allowedProps.has(propName)) {
reportError(
fileLabel,
`${pathLabel}.props.${propName}`,
`Invalid prop "${propName}" for component type "${component.type}"`
)
}
})
}
const lintComponent = (component, fileLabel, pathLabel, contextVars, dataSourceIds) => {
if (!component || typeof component !== 'object') return
if (!component.id) {
reportError(fileLabel, pathLabel, 'Missing required component id')
}
if (!component.type) {
reportError(fileLabel, pathLabel, 'Missing required component type')
return
}
const definition = definitionsByType.get(component.type)
validateProps(component, fileLabel, pathLabel, definition)
validateRequiredProps(component, fileLabel, pathLabel, definition, component.bindings)
validateBindings(component.bindings, fileLabel, pathLabel, contextVars, dataSourceIds, definition)
validateDataBinding(component.dataBinding, fileLabel, pathLabel, contextVars, dataSourceIds)
const nextContextVars = new Set(contextVars)
const repeatConfig = component.loop ?? component.repeat
if (repeatConfig) {
if (repeatConfig.itemVar) {
nextContextVars.add(repeatConfig.itemVar)
}
if (repeatConfig.indexVar) {
nextContextVars.add(repeatConfig.indexVar)
}
}
if (Array.isArray(component.children)) {
component.children.forEach((child, index) => {
if (typeof child === 'string') return
lintComponent(child, fileLabel, `${pathLabel}.children[${index}]`, nextContextVars, dataSourceIds)
})
}
if (component.conditional) {
const branches = [component.conditional.then, component.conditional.else]
branches.forEach((branch, branchIndex) => {
if (!branch) return
if (typeof branch === 'string') return
if (Array.isArray(branch)) {
branch.forEach((child, index) => {
if (typeof child === 'string') return
lintComponent(child, fileLabel, `${pathLabel}.conditional.${branchIndex}[${index}]`, nextContextVars, dataSourceIds)
})
} else {
lintComponent(branch, fileLabel, `${pathLabel}.conditional.${branchIndex}`, nextContextVars, dataSourceIds)
}
})
}
}
const lintSchema = (schema, fileLabel) => {
const dataSourceIds = new Set(
Array.isArray(schema.dataSources)
? schema.dataSources.map((source) => source.id).filter(Boolean)
: []
)
schema.components.forEach((component, index) => {
lintComponent(component, fileLabel, `components[${index}]`, new Set(), dataSourceIds)
})
}
const schemaFiles = collectSchemaFiles(schemaDirs)
schemaFiles.forEach((filePath) => {
const data = readJson(filePath)
const schemas = extractSchemas(data, filePath)
schemas.forEach(({ name, schema }) => lintSchema(schema, name))
})
if (errors.length > 0) {
console.error('JSON UI lint errors found:')
errors.forEach((error) => {
console.error(`- ${error.file} :: ${error.path} :: ${error.message}`)
})
process.exit(1)
}
console.log('JSON UI lint passed.')

View File

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

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

View File

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

@@ -0,0 +1,235 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as PhosphorIcons from '@phosphor-icons/react'
import { JSONUIShowcase } from '../src/components/JSONUIShowcase'
type ComponentType = unknown
interface JsonRegistryEntry {
name?: string
type?: string
export?: string
source?: string
status?: string
wrapperRequired?: boolean
wrapperComponent?: string
wrapperFor?: string
load?: {
export?: string
}
deprecated?: unknown
}
interface JsonComponentRegistry {
components?: JsonRegistryEntry[]
}
const sourceAliases: Record<string, Record<string, string>> = {
atoms: {
PageHeader: 'BasicPageHeader',
SearchInput: 'BasicSearchInput',
},
molecules: {},
organisms: {},
ui: {
Chart: 'ChartContainer',
Resizable: 'ResizablePanelGroup',
},
wrappers: {},
}
const explicitComponentAllowlist: Record<string, ComponentType> = {
JSONUIShowcase,
}
const getRegistryEntryKey = (entry: JsonRegistryEntry): string | undefined =>
entry.name ?? entry.type
const getRegistryEntryExportName = (entry: JsonRegistryEntry): string | undefined =>
entry.load?.export ?? entry.export ?? getRegistryEntryKey(entry)
const buildComponentMapFromExports = (
exports: Record<string, unknown>
): Record<string, ComponentType> => {
return Object.entries(exports).reduce<Record<string, ComponentType>>((acc, [key, value]) => {
if (value && (typeof value === 'function' || typeof value === 'object')) {
acc[key] = value as ComponentType
}
return acc
}, {})
}
const buildComponentMapFromModules = (
modules: Record<string, unknown>
): Record<string, ComponentType> => {
return Object.values(modules).reduce<Record<string, ComponentType>>((acc, moduleExports) => {
if (!moduleExports || typeof moduleExports !== 'object') {
return acc
}
Object.entries(buildComponentMapFromExports(moduleExports as Record<string, unknown>)).forEach(
([key, component]) => {
acc[key] = component
}
)
return acc
}, {})
}
const listFiles = async (options: {
directory: string
extensions: string[]
recursive: boolean
}): Promise<string[]> => {
const { directory, extensions, recursive } = options
const entries = await fs.readdir(directory, { withFileTypes: true })
const files: string[] = []
await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(directory, entry.name)
if (entry.isDirectory()) {
if (recursive) {
const nested = await listFiles({ directory: fullPath, extensions, recursive })
files.push(...nested)
}
return
}
if (extensions.includes(path.extname(entry.name))) {
files.push(fullPath)
}
})
)
return files
}
const importModules = async (files: string[]): Promise<Record<string, unknown>> => {
const modules: Record<string, unknown> = {}
await Promise.all(
files.map(async (file) => {
const moduleExports = await import(pathToFileURL(file).href)
modules[file] = moduleExports
})
)
return modules
}
const validateRegistry = async () => {
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(scriptDir, '..')
const registryPath = path.join(rootDir, 'json-components-registry.json')
const registryRaw = await fs.readFile(registryPath, 'utf8')
const registry = JSON.parse(registryRaw) as JsonComponentRegistry
const registryEntries = registry.components ?? []
const registryEntryByType = new Map(
registryEntries
.map((entry) => {
const entryKey = getRegistryEntryKey(entry)
return entryKey ? [entryKey, entry] : null
})
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
)
const sourceConfigs = [
{
source: 'atoms',
directory: path.join(rootDir, 'src/components/atoms'),
extensions: ['.tsx'],
recursive: false,
},
{
source: 'molecules',
directory: path.join(rootDir, 'src/components/molecules'),
extensions: ['.tsx'],
recursive: false,
},
{
source: 'organisms',
directory: path.join(rootDir, 'src/components/organisms'),
extensions: ['.tsx'],
recursive: false,
},
{
source: 'ui',
directory: path.join(rootDir, 'src/components/ui'),
extensions: ['.ts', '.tsx'],
recursive: true,
},
{
source: 'wrappers',
directory: path.join(rootDir, 'src/lib/json-ui/wrappers'),
extensions: ['.tsx'],
recursive: false,
},
]
const componentMaps: Record<string, Record<string, ComponentType>> = {}
await Promise.all(
sourceConfigs.map(async (config) => {
const files = await listFiles({
directory: config.directory,
extensions: config.extensions,
recursive: config.recursive,
})
const modules = await importModules(files)
componentMaps[config.source] = buildComponentMapFromModules(modules)
})
)
componentMaps.icons = buildComponentMapFromExports(PhosphorIcons)
const errors: string[] = []
registryEntries.forEach((entry) => {
const entryKey = getRegistryEntryKey(entry)
const entryExportName = getRegistryEntryExportName(entry)
if (!entryKey || !entryExportName) {
errors.push(`Entry missing name/type/export: ${JSON.stringify(entry)}`)
return
}
const source = entry.source
if (!source || !componentMaps[source]) {
errors.push(`${entryKey}: unknown source "${source ?? 'missing'}"`)
return
}
const aliasName = sourceAliases[source]?.[entryKey]
const component =
componentMaps[source][entryExportName] ??
(aliasName ? componentMaps[source][aliasName] : undefined) ??
explicitComponentAllowlist[entryKey]
if (!component) {
const aliasNote = aliasName ? ` (alias: ${aliasName})` : ''
errors.push(
`${entryKey} (${source}) did not resolve export "${entryExportName}"${aliasNote}`
)
}
if (entry.wrapperRequired) {
if (!entry.wrapperComponent) {
errors.push(`${entryKey} (${source}) requires a wrapperComponent but none is defined`)
return
}
if (!registryEntryByType.has(entry.wrapperComponent)) {
errors.push(
`${entryKey} (${source}) references missing wrapperComponent ${entry.wrapperComponent}`
)
}
}
})
if (errors.length > 0) {
console.error('❌ JSON component registry export validation failed:')
errors.forEach((error) => console.error(`- ${error}`))
process.exit(1)
}
console.log('✅ JSON component registry exports are valid.')
}
await validateRegistry()

View File

@@ -0,0 +1,297 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { UIComponentSchema } from '../src/lib/json-ui/schema'
interface ComponentDefinitionProp {
name: string
type: 'string' | 'number' | 'boolean'
options?: Array<string | number | boolean>
}
interface ComponentDefinition {
type: string
props?: ComponentDefinitionProp[]
}
interface ComponentNode {
component: Record<string, unknown>
path: string
}
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(__dirname, '..')
const componentDefinitionsPath = path.join(rootDir, 'src/lib/component-definitions.json')
const componentRegistryPath = path.join(rootDir, 'src/lib/json-ui/component-registry.ts')
const jsonRegistryPath = path.join(rootDir, 'json-components-registry.json')
const readJson = (filePath: string) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
const readText = (filePath: string) => fs.readFileSync(filePath, 'utf8')
const componentDefinitions = readJson(componentDefinitionsPath) as ComponentDefinition[]
const componentDefinitionMap = new Map(componentDefinitions.map((def) => [def.type, def]))
const jsonRegistry = readJson(jsonRegistryPath) as {
components?: Array<{ type?: string; name?: string; export?: string }>
}
const extractObjectLiteral = (content: string, marker: string) => {
const markerIndex = content.indexOf(marker)
if (markerIndex === -1) {
throw new Error(`Unable to locate ${marker} in component registry file`)
}
const braceStart = content.indexOf('{', markerIndex)
if (braceStart === -1) {
throw new Error(`Unable to locate opening brace for ${marker}`)
}
let depth = 0
for (let i = braceStart; i < content.length; i += 1) {
const char = content[i]
if (char === '{') depth += 1
if (char === '}') depth -= 1
if (depth === 0) {
return content.slice(braceStart, i + 1)
}
}
throw new Error(`Unable to locate closing brace for ${marker}`)
}
const extractKeysFromObjectLiteral = (literal: string) => {
const body = literal.trim().replace(/^\{/, '').replace(/\}$/, '')
const entries = body
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
const keys = new Set<string>()
entries.forEach((entry) => {
if (entry.startsWith('...')) {
return
}
const [keyPart] = entry.split(':')
const key = keyPart.trim()
if (key) {
keys.add(key)
}
})
return keys
}
const componentRegistryContent = readText(componentRegistryPath)
const primitiveKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(componentRegistryContent, 'export const primitiveComponents')
)
const shadcnKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(componentRegistryContent, 'export const shadcnComponents')
)
const wrapperKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(componentRegistryContent, 'export const jsonWrapperComponents')
)
const iconKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(componentRegistryContent, 'export const iconComponents')
)
const registryTypes = new Set<string>(
(jsonRegistry.components ?? [])
.map((entry) => entry.type ?? entry.name ?? entry.export)
.filter((value): value is string => Boolean(value))
)
const validComponentTypes = new Set<string>([
...primitiveKeys,
...shadcnKeys,
...wrapperKeys,
...iconKeys,
...componentDefinitions.map((def) => def.type),
...registryTypes,
])
const schemaRoots = [
path.join(rootDir, 'src/config'),
path.join(rootDir, 'src/data'),
]
const collectJsonFiles = (dir: string, files: string[] = []) => {
if (!fs.existsSync(dir)) {
return files
}
const entries = fs.readdirSync(dir, { withFileTypes: true })
entries.forEach((entry) => {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
collectJsonFiles(fullPath, files)
return
}
if (entry.isFile() && entry.name.endsWith('.json')) {
files.push(fullPath)
}
})
return files
}
const isComponentNode = (value: unknown): value is Record<string, unknown> => {
if (!value || typeof value !== 'object') {
return false
}
const candidate = value as Record<string, unknown>
if (typeof candidate.id !== 'string' || typeof candidate.type !== 'string') {
return false
}
return (
'props' in candidate ||
'children' in candidate ||
'className' in candidate ||
'bindings' in candidate ||
'events' in candidate ||
'dataBinding' in candidate ||
'style' in candidate
)
}
const findComponents = (value: unknown, currentPath: string): ComponentNode[] => {
const components: ComponentNode[] = []
if (Array.isArray(value)) {
value.forEach((item, index) => {
components.push(...findComponents(item, `${currentPath}[${index}]`))
})
return components
}
if (!value || typeof value !== 'object') {
return components
}
const candidate = value as Record<string, unknown>
if (isComponentNode(candidate)) {
components.push({ component: candidate, path: currentPath })
}
Object.entries(candidate).forEach(([key, child]) => {
const nextPath = currentPath ? `${currentPath}.${key}` : key
components.push(...findComponents(child, nextPath))
})
return components
}
const isTemplateBinding = (value: unknown) =>
typeof value === 'string' && value.includes('{{') && value.includes('}}')
const validateProps = (
component: Record<string, unknown>,
filePath: string,
componentPath: string,
errors: string[]
) => {
const definition = componentDefinitionMap.get(component.type as string)
const props = component.props
if (!definition || !definition.props || !props || typeof props !== 'object') {
return
}
const propDefinitions = new Map(definition.props.map((prop) => [prop.name, prop]))
Object.entries(props as Record<string, unknown>).forEach(([propName, propValue]) => {
const propDefinition = propDefinitions.get(propName)
if (!propDefinition) {
errors.push(
`${filePath} -> ${componentPath}: Unknown prop "${propName}" for component type "${component.type}"`
)
return
}
const expectedType = propDefinition.type
const actualType = Array.isArray(propValue) ? 'array' : typeof propValue
if (
expectedType === 'string' &&
actualType !== 'string' &&
propValue !== undefined
) {
errors.push(
`${filePath} -> ${componentPath}: Prop "${propName}" expected string but got ${actualType}`
)
return
}
if (
expectedType === 'number' &&
actualType !== 'number' &&
!isTemplateBinding(propValue)
) {
errors.push(
`${filePath} -> ${componentPath}: Prop "${propName}" expected number but got ${actualType}`
)
return
}
if (
expectedType === 'boolean' &&
actualType !== 'boolean' &&
!isTemplateBinding(propValue)
) {
errors.push(
`${filePath} -> ${componentPath}: Prop "${propName}" expected boolean but got ${actualType}`
)
return
}
if (propDefinition.options && propValue !== undefined) {
if (!propDefinition.options.includes(propValue as string | number | boolean)) {
errors.push(
`${filePath} -> ${componentPath}: Prop "${propName}" value must be one of ${propDefinition.options.join(', ')}`
)
}
}
})
}
const validateComponentsInFile = (filePath: string, errors: string[]) => {
let parsed: unknown
try {
parsed = readJson(filePath)
} catch (error) {
errors.push(`${filePath}: Unable to parse JSON - ${(error as Error).message}`)
return
}
const components = findComponents(parsed, 'root')
if (components.length === 0) {
return
}
components.forEach(({ component, path: componentPath }) => {
const parseResult = UIComponentSchema.safeParse(component)
if (!parseResult.success) {
const issueMessages = parseResult.error.issues
.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`)
.join('\n')
errors.push(
`${filePath} -> ${componentPath}: Schema validation failed\n${issueMessages}`
)
}
if (!validComponentTypes.has(component.type as string)) {
errors.push(
`${filePath} -> ${componentPath}: Unknown component type "${component.type}"`
)
}
validateProps(component, filePath, componentPath, errors)
})
}
const jsonFiles = schemaRoots.flatMap((dir) => collectJsonFiles(dir))
const errors: string[] = []
jsonFiles.forEach((filePath) => validateComponentsInFile(filePath, errors))
if (errors.length > 0) {
console.error('JSON schema validation failed:')
errors.forEach((error) => console.error(`- ${error}`))
process.exit(1)
}
console.log('JSON schema validation passed.')

View File

@@ -0,0 +1,176 @@
const fs = require('fs')
const path = require('path')
const rootDir = path.resolve(__dirname, '..')
const registryPath = path.join(rootDir, 'json-components-registry.json')
const definitionsPath = path.join(rootDir, 'src/lib/component-definitions.json')
const componentTypesPath = path.join(rootDir, 'src/types/json-ui-component-types.ts')
const uiRegistryPath = path.join(rootDir, 'src/lib/json-ui/component-registry.ts')
const atomIndexPath = path.join(rootDir, 'src/components/atoms/index.ts')
const moleculeIndexPath = path.join(rootDir, 'src/components/molecules/index.ts')
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
const readText = (filePath) => fs.readFileSync(filePath, 'utf8')
const registryData = readJson(registryPath)
const supportedComponents = (registryData.components ?? []).filter(
(component) => component.status === 'supported'
)
const componentDefinitions = readJson(definitionsPath)
const definitionTypes = new Set(componentDefinitions.map((def) => def.type))
const componentTypesContent = readText(componentTypesPath)
const componentTypeSet = new Set()
const componentTypeRegex = /"([^"]+)"/g
let match
while ((match = componentTypeRegex.exec(componentTypesContent)) !== null) {
componentTypeSet.add(match[1])
}
const extractObjectLiteral = (content, marker) => {
const markerIndex = content.indexOf(marker)
if (markerIndex === -1) {
throw new Error(`Unable to locate ${marker} in component registry file`)
}
const braceStart = content.indexOf('{', markerIndex)
if (braceStart === -1) {
throw new Error(`Unable to locate opening brace for ${marker}`)
}
let depth = 0
for (let i = braceStart; i < content.length; i += 1) {
const char = content[i]
if (char === '{') depth += 1
if (char === '}') depth -= 1
if (depth === 0) {
return content.slice(braceStart, i + 1)
}
}
throw new Error(`Unable to locate closing brace for ${marker}`)
}
const extractKeysFromObjectLiteral = (literal) => {
const body = literal.trim().replace(/^\{/, '').replace(/\}$/, '')
const entries = body
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
const keys = new Set()
entries.forEach((entry) => {
if (entry.startsWith('...')) {
return
}
const [keyPart] = entry.split(':')
const key = keyPart.trim()
if (key) {
keys.add(key)
}
})
return keys
}
const uiRegistryContent = readText(uiRegistryPath)
const primitiveKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(uiRegistryContent, 'export const primitiveComponents')
)
const shadcnKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(uiRegistryContent, 'export const shadcnComponents')
)
const wrapperKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(uiRegistryContent, 'export const jsonWrapperComponents')
)
const iconKeys = extractKeysFromObjectLiteral(
extractObjectLiteral(uiRegistryContent, 'export const iconComponents')
)
const extractExports = (content) => {
const exportsSet = new Set()
const exportRegex = /export\s+\{([^}]+)\}\s+from/g
let exportMatch
while ((exportMatch = exportRegex.exec(content)) !== null) {
const names = exportMatch[1]
.split(',')
.map((name) => name.trim())
.filter(Boolean)
names.forEach((name) => {
const [exportName] = name.split(/\s+as\s+/)
if (exportName) {
exportsSet.add(exportName.trim())
}
})
}
return exportsSet
}
const atomExports = extractExports(readText(atomIndexPath))
const moleculeExports = extractExports(readText(moleculeIndexPath))
const uiRegistryKeys = new Set([
...primitiveKeys,
...shadcnKeys,
...wrapperKeys,
...iconKeys,
...atomExports,
...moleculeExports,
])
const missingInTypes = []
const missingInDefinitions = []
const missingInRegistry = []
supportedComponents.forEach((component) => {
const typeName = component.type ?? component.name ?? component.export
const registryName = component.export ?? component.name ?? component.type
if (!typeName) {
return
}
if (!componentTypeSet.has(typeName)) {
missingInTypes.push(typeName)
}
if (!definitionTypes.has(typeName)) {
missingInDefinitions.push(typeName)
}
const source = component.source ?? 'unknown'
let registryHasComponent = uiRegistryKeys.has(registryName)
if (source === 'atoms') {
registryHasComponent = atomExports.has(registryName)
}
if (source === 'molecules') {
registryHasComponent = moleculeExports.has(registryName)
}
if (source === 'ui') {
registryHasComponent = shadcnKeys.has(registryName)
}
if (!registryHasComponent) {
missingInRegistry.push(`${registryName} (${source})`)
}
})
const unique = (list) => Array.from(new Set(list)).sort()
const errors = []
if (missingInTypes.length > 0) {
errors.push(`Missing in ComponentType union: ${unique(missingInTypes).join(', ')}`)
}
if (missingInDefinitions.length > 0) {
errors.push(`Missing in component definitions: ${unique(missingInDefinitions).join(', ')}`)
}
if (missingInRegistry.length > 0) {
errors.push(`Missing in UI registry mapping: ${unique(missingInRegistry).join(', ')}`)
}
if (errors.length > 0) {
console.error('Supported component validation failed:')
errors.forEach((error) => console.error(`- ${error}`))
process.exit(1)
}
console.log('Supported component validation passed.')

View File

@@ -3,7 +3,8 @@ import { useToggle, useDialog } from '@/hooks/ui'
import { useKV } from '@/hooks/use-kv'
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { SearchInput, DataCard, ActionBar } from '@/components/molecules'
import { SearchInput } from '@/components/molecules'
import { DataCard } from '@/components/atoms/json-ui'
import { Grid, Heading, StatusBadge } from '@/components/atoms'
import { Plus, Trash, Eye } from '@phosphor-icons/react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
@@ -64,28 +65,31 @@ export function AtomicComponentDemo() {
</div>
<Grid cols={3} gap={4}>
<DataCard title="Total Tasks" value={stats.total} />
<DataCard title="Active" value={stats.active} />
<DataCard title="Completed" value={stats.completed} />
<DataCard title="Total Tasks" icon="list" gradient="from-blue-500/10 to-blue-500/5">
<div className="text-3xl font-bold">{stats.total}</div>
</DataCard>
<DataCard title="Active" icon="clock" gradient="from-amber-500/10 to-amber-500/5">
<div className="text-3xl font-bold">{stats.active}</div>
</DataCard>
<DataCard title="Completed" icon="check" gradient="from-green-500/10 to-green-500/5">
<div className="text-3xl font-bold">{stats.completed}</div>
</DataCard>
</Grid>
<ActionBar
title="Tasks"
actions={[
{
label: 'Add Task',
icon: <Plus size={16} />,
onClick: addDialog.open,
variant: 'default',
},
{
label: showCompleted.value ? 'Hide Completed' : 'Show Completed',
icon: <Eye size={16} />,
onClick: showCompleted.toggle,
variant: 'outline',
},
]}
/>
{/* ActionBar replaced with inline buttons */}
<div className="flex items-center justify-between">
<Heading level={3}>Tasks</Heading>
<div className="flex gap-2">
<Button onClick={addDialog.open} size="sm">
<Plus size={16} className="mr-2" />
Add Task
</Button>
<Button onClick={showCompleted.toggle} variant="outline" size="sm">
<Eye size={16} className="mr-2" />
{showCompleted.value ? 'Hide Completed' : 'Show Completed'}
</Button>
</div>
</div>
<SearchInput
value={query}

View File

@@ -1,36 +1,15 @@
import { useState } from 'react'
import { DataSourceManager } from '@/components/organisms/DataSourceManager'
import { ComponentBindingDialog } from '@/components/molecules/ComponentBindingDialog'
import { ComponentBindingDialog } from '@/lib/json-ui/json-components'
import { DataSource, UIComponent } from '@/types/json-ui'
import { DataBindingHeader } from '@/components/data-binding-designer/DataBindingHeader'
import { ComponentBindingsCard } from '@/components/data-binding-designer/ComponentBindingsCard'
import { HowItWorksCard } from '@/components/data-binding-designer/HowItWorksCard'
import dataBindingCopy from '@/data/data-binding-designer.json'
interface SeedDataSource extends Omit<DataSource, 'compute'> {
computeId?: string
}
const computeRegistry: Record<string, (data: Record<string, any>) => any> = {
displayName: (data) => `Welcome, ${data.userProfile?.name || 'Guest'}!`,
}
const buildSeedDataSources = (sources: SeedDataSource[]): DataSource[] => {
return sources.map((source) => {
if (source.type === 'computed' && source.computeId) {
return {
...source,
compute: computeRegistry[source.computeId],
}
}
return source
})
}
export function DataBindingDesigner() {
const [dataSources, setDataSources] = useState<DataSource[]>(
buildSeedDataSources(dataBindingCopy.seed.dataSources as SeedDataSource[]),
dataBindingCopy.seed.dataSources as DataSource[],
)
const [mockComponents] = useState<UIComponent[]>(dataBindingCopy.seed.components)

View File

@@ -15,3 +15,16 @@ export const PRIORITY_COLORS = {
medium: 'border-amber-400/60 bg-amber-50/80 dark:bg-amber-950/40',
high: 'border-red-400/60 bg-red-50/80 dark:bg-red-950/40',
}
// Missing exports for GROUP_COLORS, CATEGORIES, PRIORITIES, STATUSES
export const GROUP_COLORS = {
default: '#a78bfa',
primary: '#60a5fa',
success: '#34d399',
warning: '#fbbf24',
danger: '#f87171',
}
export const CATEGORIES = ['feature', 'enhancement', 'bug', 'documentation', 'other'] as const
export const PRIORITIES = ['low', 'medium', 'high'] as const
export const STATUSES = ['idea', 'planned', 'in-progress', 'completed'] as const

View File

@@ -0,0 +1,9 @@
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import conversionShowcaseSchema from '@/config/pages/json-conversion-showcase.json'
import { PageSchema } from '@/types/json-ui'
export function JSONConversionShowcase() {
const schema = conversionShowcaseSchema as PageSchema
return <PageRenderer schema={schema} />
}

View File

@@ -1,16 +1,9 @@
import { PageRenderer } from '@/lib/schema-renderer'
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import lambdaDesignerSchema from '@/config/pages/lambda-designer.json'
import { useKV } from '@/hooks/use-kv'
import { Component as ComponentSchema } from '@/schemas/ui-schema'
import { PageSchema } from '@/types/json-ui'
export function JSONLambdaDesigner() {
const [lambdas] = useKV('app-lambdas', [])
return (
<PageRenderer
schema={lambdaDesignerSchema as ComponentSchema}
data={{ lambdas }}
functions={{}}
/>
<PageRenderer schema={lambdaDesignerSchema as PageSchema} />
)
}

View File

@@ -0,0 +1,33 @@
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import { useSchemaLoader } from '@/hooks/use-schema-loader'
interface JSONSchemaPageLoaderProps {
schemaPath: string
data?: Record<string, any>
functions?: Record<string, any>
}
export function JSONSchemaPageLoader({ schemaPath, data, functions }: JSONSchemaPageLoaderProps) {
const { schema, loading, error } = useSchemaLoader(schemaPath)
if (loading) {
return (
<div className="flex items-center justify-center h-full w-full">
<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 {schemaPath}...</p>
</div>
</div>
)
}
if (error || !schema) {
return (
<div className="p-8 text-center">
<p className="text-destructive">{error || 'Schema not found'}</p>
</div>
)
}
return <PageRenderer schema={schema} data={data} functions={functions} />
}

View File

@@ -78,6 +78,16 @@ export function JSONUIPage({ jsonConfig }: JSONUIPageProps) {
updateDataField('formData', action.params.field, event)
}
break
case 'update-date':
if (action.params?.field) {
updateDataField('formData', action.params.field, event)
}
break
case 'update-files':
if (action.params?.field) {
updateDataField('formData', action.params.field, event)
}
break
case 'submit-form':
toast.success('Form submitted!')
console.log('Form data:', dataMap.formData)

View File

@@ -1,37 +1,35 @@
import { useMemo, useState } from 'react'
import showcaseCopy from '@/config/ui-examples/showcase.json'
import dashboardExample from '@/config/ui-examples/dashboard.json'
import formExample from '@/config/ui-examples/form.json'
import tableExample from '@/config/ui-examples/table.json'
import settingsExample from '@/config/ui-examples/settings.json'
import { FileCode, ChartBar, ListBullets, Table, Gear } from '@phosphor-icons/react'
import { FileCode, ChartBar, ListBullets, Table, Gear, Clock } from '@phosphor-icons/react'
import { ShowcaseHeader } from '@/components/json-ui-showcase/ShowcaseHeader'
import { ShowcaseTabs } from '@/components/json-ui-showcase/ShowcaseTabs'
import { ShowcaseFooter } from '@/components/json-ui-showcase/ShowcaseFooter'
import { ShowcaseExample } from '@/components/json-ui-showcase/types'
const exampleConfigs = {
dashboard: dashboardExample,
form: formExample,
table: tableExample,
settings: settingsExample,
}
const exampleIcons = {
ChartBar,
ListBullets,
Table,
Clock,
Gear,
}
const configModules = import.meta.glob('/src/config/ui-examples/*.json', { eager: true })
const resolveExampleConfig = (configPath: string) => {
const moduleEntry = configModules[configPath] as { default: ShowcaseExample['config'] } | undefined
return moduleEntry?.default ?? {}
}
export function JSONUIShowcase() {
const [selectedExample, setSelectedExample] = useState(showcaseCopy.defaultExampleKey)
const [showJSON, setShowJSON] = useState(false)
const examples = useMemo<ShowcaseExample[]>(() => {
return showcaseCopy.examples.map((example) => {
const icon = exampleIcons[example.icon as keyof typeof exampleIcons] || FileCode
const config = exampleConfigs[example.configKey as keyof typeof exampleConfigs]
const icon = exampleIcons[example.iconId as keyof typeof exampleIcons] || FileCode
const config = resolveExampleConfig(example.configPath)
return {
key: example.key,

View File

@@ -3,11 +3,13 @@ import { AtomicComponentDemo } from '@/components/AtomicComponentDemo'
import { DashboardDemoPage } from '@/components/DashboardDemoPage'
import { PageRenderer } from '@/lib/json-ui/page-renderer'
import { hydrateSchema } from '@/schemas/schema-loader'
import pageSchemasJson from '@/schemas/page-schemas.json'
import todoListJson from '@/schemas/todo-list.json'
import newMoleculesShowcaseJson from '@/schemas/new-molecules-showcase.json'
const todoListSchema = hydrateSchema(todoListJson)
const newMoleculesShowcaseSchema = hydrateSchema(newMoleculesShowcaseJson)
const dataComponentsDemoSchema = hydrateSchema(pageSchemasJson.dataComponentsDemoSchema)
export function JSONUIShowcasePage() {
return (
@@ -24,7 +26,9 @@ export function JSONUIShowcasePage() {
</div>
<TabsList className="w-full justify-start">
<TabsTrigger value="atomic">Atomic Components</TabsTrigger>
<TabsTrigger value="feedback">Feedback Atoms</TabsTrigger>
<TabsTrigger value="molecules">New Molecules</TabsTrigger>
<TabsTrigger value="data-components">Data Components</TabsTrigger>
<TabsTrigger value="dashboard">JSON Dashboard</TabsTrigger>
<TabsTrigger value="todos">JSON Todo List</TabsTrigger>
</TabsList>
@@ -34,10 +38,18 @@ export function JSONUIShowcasePage() {
<TabsContent value="atomic" className="h-full m-0 data-[state=active]:block">
<AtomicComponentDemo />
</TabsContent>
<TabsContent value="feedback" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={feedbackAtomsDemoSchema} />
</TabsContent>
<TabsContent value="molecules" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={newMoleculesShowcaseSchema} />
</TabsContent>
<TabsContent value="data-components" className="h-full m-0 data-[state=active]:block">
<PageRenderer schema={dataComponentsDemoSchema} />
</TabsContent>
<TabsContent value="dashboard" className="h-full m-0 data-[state=active]:block">
<DashboardDemoPage />

View File

@@ -45,11 +45,12 @@ function getCompletionMessage(score: number): string {
}
export function ProjectDashboard(props: ProjectDashboardProps) {
const completionMetrics = calculateCompletionScore(props)
return (
<JSONPageRenderer
schema={dashboardSchema as any}
data={props}
functions={{ calculateCompletionScore }}
data={{ ...props, ...completionMetrics }}
/>
)
}

View File

@@ -1,68 +0,0 @@
import { useState } from 'react'
import { CaretDown } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface AccordionItem {
id: string
title: string
content: React.ReactNode
disabled?: boolean
}
interface AccordionProps {
items: AccordionItem[]
type?: 'single' | 'multiple'
defaultOpen?: string[]
className?: string
}
export function Accordion({ items, type = 'single', defaultOpen = [], className }: AccordionProps) {
const [openItems, setOpenItems] = useState<string[]>(defaultOpen)
const toggleItem = (id: string) => {
if (type === 'single') {
setOpenItems(openItems.includes(id) ? [] : [id])
} else {
setOpenItems(
openItems.includes(id)
? openItems.filter((item) => item !== id)
: [...openItems, id]
)
}
}
return (
<div className={cn('space-y-2', className)}>
{items.map((item) => {
const isOpen = openItems.includes(item.id)
return (
<div key={item.id} className="border border-border rounded-lg overflow-hidden">
<button
onClick={() => !item.disabled && toggleItem(item.id)}
disabled={item.disabled}
className={cn(
'w-full flex items-center justify-between p-4 bg-card text-card-foreground font-medium transition-colors',
'hover:bg-accent hover:text-accent-foreground',
item.disabled && 'opacity-50 cursor-not-allowed'
)}
>
<span>{item.title}</span>
<CaretDown
className={cn(
'w-5 h-5 transition-transform',
isOpen && 'rotate-180'
)}
/>
</button>
{isOpen && (
<div className="p-4 bg-card border-t border-border animate-in slide-in-from-top-2">
{item.content}
</div>
)}
</div>
)
})}
</div>
)
}

View File

@@ -1,51 +0,0 @@
import { ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
export interface ActionButtonProps {
icon?: ReactNode
label: string
onClick: () => void
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
size?: 'default' | 'sm' | 'lg' | 'icon'
tooltip?: string
disabled?: boolean
className?: string
}
export function ActionButton({
icon,
label,
onClick,
variant = 'default',
size = 'default',
tooltip,
disabled,
className,
}: ActionButtonProps) {
const button = (
<Button
variant={variant}
size={size}
onClick={onClick}
disabled={disabled}
className={className}
>
{icon && <span className="mr-2">{icon}</span>}
{label}
</Button>
)
if (tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
return button
}

View File

@@ -1,42 +0,0 @@
import { cn } from '@/lib/utils'
import { Card, CardContent } from '@/components/ui/card'
import { CaretRight } from '@phosphor-icons/react'
interface ActionCardProps {
icon?: React.ReactNode
title: string
description?: string
onClick?: () => void
className?: string
disabled?: boolean
}
export function ActionCard({ icon, title, description, onClick, className, disabled }: ActionCardProps) {
return (
<Card
className={cn(
'cursor-pointer transition-all hover:shadow-md hover:border-primary/50',
disabled && 'opacity-50 cursor-not-allowed',
className
)}
onClick={disabled ? undefined : onClick}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{icon && (
<div className="flex-shrink-0 p-2 rounded-lg bg-primary/10 text-primary">
{icon}
</div>
)}
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm mb-1">{title}</div>
{description && (
<div className="text-xs text-muted-foreground line-clamp-2">{description}</div>
)}
</div>
<CaretRight size={16} className="flex-shrink-0 text-muted-foreground" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,22 +0,0 @@
import { Plus, Pencil, Trash, Copy, Download, Upload } from '@phosphor-icons/react'
interface ActionIconProps {
action: 'add' | 'edit' | 'delete' | 'copy' | 'download' | 'upload'
size?: number
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
className?: string
}
export function ActionIcon({ action, size = 16, weight = 'regular', className = '' }: ActionIconProps) {
const iconMap = {
add: Plus,
edit: Pencil,
delete: Trash,
copy: Copy,
download: Download,
upload: Upload,
}
const IconComponent = iconMap[action]
return <IconComponent size={size} weight={weight} className={className} />
}

View File

@@ -1,51 +0,0 @@
import { ReactNode } from 'react'
import { Info, Warning, CheckCircle, XCircle } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface AlertProps {
variant?: 'info' | 'warning' | 'success' | 'error'
title?: string
children: ReactNode
className?: string
}
const variantConfig = {
info: {
icon: Info,
classes: 'bg-blue-50 border-blue-200 text-blue-900',
},
warning: {
icon: Warning,
classes: 'bg-yellow-50 border-yellow-200 text-yellow-900',
},
success: {
icon: CheckCircle,
classes: 'bg-green-50 border-green-200 text-green-900',
},
error: {
icon: XCircle,
classes: 'bg-red-50 border-red-200 text-red-900',
},
}
export function Alert({ variant = 'info', title, children, className }: AlertProps) {
const config = variantConfig[variant]
const Icon = config.icon
return (
<div
className={cn(
'flex gap-3 p-4 rounded-lg border',
config.classes,
className
)}
role="alert"
>
<Icon size={20} weight="bold" className="flex-shrink-0 mt-0.5" />
<div className="flex-1">
{title && <div className="font-semibold mb-1">{title}</div>}
<div className="text-sm">{children}</div>
</div>
</div>
)
}

View File

@@ -1,9 +0,0 @@
import { Code } from '@phosphor-icons/react'
export function AppLogo() {
return (
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shrink-0">
<Code size={20} weight="duotone" className="text-white sm:w-6 sm:h-6" />
</div>
)
}

View File

@@ -1,37 +0,0 @@
import { cn } from '@/lib/utils'
interface AvatarProps {
src?: string
alt?: string
fallback?: string
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
className?: string
}
const sizeClasses = {
xs: 'w-6 h-6 text-xs',
sm: 'w-8 h-8 text-sm',
md: 'w-10 h-10 text-base',
lg: 'w-12 h-12 text-lg',
xl: 'w-16 h-16 text-xl',
}
export function Avatar({ src, alt, fallback, size = 'md', className }: AvatarProps) {
const initials = fallback || alt?.slice(0, 2).toUpperCase() || '?'
return (
<div
className={cn(
'relative inline-flex items-center justify-center rounded-full bg-muted overflow-hidden',
sizeClasses[size],
className
)}
>
{src ? (
<img src={src} alt={alt} className="w-full h-full object-cover" />
) : (
<span className="font-medium text-muted-foreground">{initials}</span>
)}
</div>
)
}

View File

@@ -1,60 +0,0 @@
import { cn } from '@/lib/utils'
interface AvatarGroupProps {
avatars: {
src?: string
alt: string
fallback: string
}[]
max?: number
size?: 'xs' | 'sm' | 'md' | 'lg'
className?: string
}
const sizeClasses = {
xs: 'h-6 w-6 text-xs',
sm: 'h-8 w-8 text-xs',
md: 'h-10 w-10 text-sm',
lg: 'h-12 w-12 text-base',
}
export function AvatarGroup({
avatars,
max = 5,
size = 'md',
className,
}: AvatarGroupProps) {
const displayAvatars = avatars.slice(0, max)
const remainingCount = Math.max(avatars.length - max, 0)
return (
<div className={cn('flex -space-x-2', className)}>
{displayAvatars.map((avatar, index) => (
<div
key={index}
className={cn(
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted overflow-hidden',
sizeClasses[size]
)}
title={avatar.alt}
>
{avatar.src ? (
<img src={avatar.src} alt={avatar.alt} className="h-full w-full object-cover" />
) : (
<span className="font-medium text-foreground">{avatar.fallback}</span>
)}
</div>
))}
{remainingCount > 0 && (
<div
className={cn(
'relative inline-flex items-center justify-center rounded-full border-2 border-background bg-muted',
sizeClasses[size]
)}
>
<span className="font-medium text-foreground">+{remainingCount}</span>
</div>
)}
</div>
)
}

View File

@@ -1,39 +0,0 @@
import { Badge as ShadcnBadge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface BadgeProps {
children: ReactNode
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
icon?: ReactNode
className?: string
}
const sizeClasses = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-2.5 py-0.5',
lg: 'text-base px-3 py-1',
}
export function Badge({
children,
variant = 'default',
size = 'md',
icon,
className,
}: BadgeProps) {
return (
<ShadcnBadge
variant={variant}
className={cn(
'inline-flex items-center gap-1.5',
sizeClasses[size],
className
)}
>
{icon && <span className="flex-shrink-0">{icon}</span>}
{children}
</ShadcnBadge>
)
}

View File

@@ -1,28 +0,0 @@
import { Link } from '@phosphor-icons/react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
interface BindingIndicatorProps {
sourceId: string
path?: string
className?: string
}
export function BindingIndicator({ sourceId, path, className = '' }: BindingIndicatorProps) {
const bindingText = path ? `${sourceId}.${path}` : sourceId
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-accent/10 text-accent border border-accent/30 ${className}`}>
<Link weight="bold" className="w-3 h-3" />
<span className="font-mono">{bindingText}</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Bound to: {bindingText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -1,51 +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
return (
<div key={index} className="flex items-center gap-2">
{item.href || item.onClick ? (
<button
onClick={item.onClick}
className={cn(
'text-sm transition-colors',
isLast
? 'text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground'
)}
>
{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>
)
}

View File

@@ -1,43 +0,0 @@
import { Button as ShadcnButton, ButtonProps as ShadcnButtonProps } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
export interface ButtonProps extends ShadcnButtonProps {
children: ReactNode
leftIcon?: ReactNode
rightIcon?: ReactNode
loading?: boolean
fullWidth?: boolean
}
export function Button({
children,
leftIcon,
rightIcon,
loading,
fullWidth,
disabled,
className,
...props
}: ButtonProps) {
return (
<ShadcnButton
disabled={disabled || loading}
className={cn(fullWidth && 'w-full', className)}
{...props}
>
{loading ? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
<span>{children}</span>
</div>
) : (
<div className="flex items-center gap-2">
{leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
<span>{children}</span>
{rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
</div>
)}
</ShadcnButton>
)
}

View File

@@ -1,33 +0,0 @@
import { cn } from '@/lib/utils'
import { ReactNode } from 'react'
interface ButtonGroupProps {
children: ReactNode
orientation?: 'horizontal' | 'vertical'
className?: string
}
export function ButtonGroup({
children,
orientation = 'horizontal',
className,
}: ButtonGroupProps) {
return (
<div
className={cn(
'inline-flex',
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
'[&>button]:rounded-none',
'[&>button:first-child]:rounded-l-md',
'[&>button:last-child]:rounded-r-md',
orientation === 'vertical' && '[&>button:first-child]:rounded-t-md [&>button:first-child]:rounded-l-none',
orientation === 'vertical' && '[&>button:last-child]:rounded-b-md [&>button:last-child]:rounded-r-none',
'[&>button:not(:last-child)]:border-r-0',
orientation === 'vertical' && '[&>button:not(:last-child)]:border-b-0 [&>button:not(:last-child)]:border-r',
className
)}
>
{children}
</div>
)
}

View File

@@ -1,28 +0,0 @@
import { Calendar as ShadcnCalendar } from '@/components/ui/calendar'
import { cn } from '@/lib/utils'
interface CalendarProps {
selected?: Date
onSelect?: (date: Date | undefined) => void
mode?: 'single' | 'multiple' | 'range'
disabled?: Date | ((date: Date) => boolean)
className?: string
}
export function Calendar({
selected,
onSelect,
mode = 'single',
disabled,
className,
}: CalendarProps) {
return (
<ShadcnCalendar
mode={mode as any}
selected={selected}
onSelect={onSelect as any}
disabled={disabled}
className={cn('rounded-md border', className)}
/>
)
}

View File

@@ -1,49 +0,0 @@
import { cn } from '@/lib/utils'
interface CardProps {
children: React.ReactNode
variant?: 'default' | 'bordered' | 'elevated' | 'flat'
padding?: 'none' | 'sm' | 'md' | 'lg'
hover?: boolean
className?: string
onClick?: () => void
}
export function Card({
children,
variant = 'default',
padding = 'md',
hover = false,
className,
onClick
}: CardProps) {
const variantStyles = {
default: 'bg-card border border-border',
bordered: 'bg-background border-2 border-border',
elevated: 'bg-card shadow-lg border border-border',
flat: 'bg-muted',
}
const paddingStyles = {
none: 'p-0',
sm: 'p-3',
md: 'p-6',
lg: 'p-8',
}
return (
<div
onClick={onClick}
className={cn(
'rounded-lg transition-all',
variantStyles[variant],
paddingStyles[padding],
hover && 'hover:shadow-md hover:scale-[1.01] cursor-pointer',
onClick && 'cursor-pointer',
className
)}
>
{children}
</div>
)
}

View File

@@ -1,60 +0,0 @@
import { Check, Minus } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface CheckboxProps {
checked: boolean
onChange: (checked: boolean) => void
label?: string
indeterminate?: boolean
disabled?: boolean
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function Checkbox({
checked,
onChange,
label,
indeterminate = false,
disabled = false,
size = 'md',
className
}: CheckboxProps) {
const sizeStyles = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
}
const iconSize = {
sm: 12,
md: 16,
lg: 20,
}
return (
<label className={cn('flex items-center gap-2 cursor-pointer', disabled && 'opacity-50 cursor-not-allowed', className)}>
<button
type="button"
role="checkbox"
aria-checked={indeterminate ? 'mixed' : checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={cn(
'flex items-center justify-center rounded border-2 transition-colors',
sizeStyles[size],
checked || indeterminate
? 'bg-primary border-primary text-primary-foreground'
: 'bg-background border-input hover:border-ring'
)}
>
{indeterminate ? (
<Minus size={iconSize[size]} weight="bold" />
) : checked ? (
<Check size={iconSize[size]} weight="bold" />
) : null}
</button>
{label && <span className="text-sm font-medium select-none">{label}</span>}
</label>
)
}

View File

@@ -1,54 +0,0 @@
import { ReactNode } from 'react'
import { X } from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface ChipProps {
children: ReactNode
variant?: 'default' | 'primary' | 'accent' | 'muted'
size?: 'sm' | 'md'
onRemove?: () => void
className?: string
}
const variantClasses = {
default: 'bg-secondary text-secondary-foreground',
primary: 'bg-primary text-primary-foreground',
accent: 'bg-accent text-accent-foreground',
muted: 'bg-muted text-muted-foreground',
}
const sizeClasses = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-3 py-1 text-sm',
}
export function Chip({
children,
variant = 'default',
size = 'md',
onRemove,
className
}: ChipProps) {
return (
<span
className={cn(
'inline-flex items-center gap-1 rounded-full font-medium',
variantClasses[variant],
sizeClasses[size],
className
)}
>
{children}
{onRemove && (
<button
type="button"
onClick={onRemove}
className="inline-flex items-center justify-center hover:bg-black/10 rounded-full transition-colors"
aria-label="Remove"
>
<X size={size === 'sm' ? 12 : 14} weight="bold" />
</button>
)}
</span>
)
}

View File

@@ -1,67 +0,0 @@
import { Progress } from '@/components/ui/progress'
import { cn } from '@/lib/utils'
interface CircularProgressProps {
value: number
max?: number
size?: 'sm' | 'md' | 'lg' | 'xl'
showLabel?: boolean
strokeWidth?: number
className?: string
}
const sizeClasses = {
sm: { dimension: 48, stroke: 4, fontSize: 'text-xs' },
md: { dimension: 64, stroke: 5, fontSize: 'text-sm' },
lg: { dimension: 96, stroke: 6, fontSize: 'text-base' },
xl: { dimension: 128, stroke: 8, fontSize: 'text-lg' },
}
export function CircularProgress({
value,
max = 100,
size = 'md',
showLabel = true,
strokeWidth,
className,
}: CircularProgressProps) {
const { dimension, stroke, fontSize } = sizeClasses[size]
const actualStroke = strokeWidth || stroke
const percentage = Math.min((value / max) * 100, 100)
const radius = (dimension - actualStroke) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (percentage / 100) * circumference
return (
<div className={cn('relative inline-flex items-center justify-center', className)}>
<svg width={dimension} height={dimension} className="transform -rotate-90">
<circle
cx={dimension / 2}
cy={dimension / 2}
r={radius}
stroke="currentColor"
strokeWidth={actualStroke}
fill="none"
className="text-muted opacity-20"
/>
<circle
cx={dimension / 2}
cy={dimension / 2}
r={radius}
stroke="currentColor"
strokeWidth={actualStroke}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
className="text-primary transition-all duration-300"
/>
</svg>
{showLabel && (
<span className={cn('absolute font-semibold', fontSize)}>
{Math.round(percentage)}%
</span>
)}
</div>
)
}

View File

@@ -1,34 +0,0 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface CodeProps {
children: ReactNode
inline?: boolean
className?: string
}
export function Code({ children, inline = true, className }: CodeProps) {
if (inline) {
return (
<code
className={cn(
'px-1.5 py-0.5 rounded bg-muted text-foreground font-mono text-sm',
className
)}
>
{children}
</code>
)
}
return (
<pre
className={cn(
'p-4 rounded-lg bg-muted text-foreground font-mono text-sm overflow-x-auto',
className
)}
>
<code>{children}</code>
</pre>
)
}

View File

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

View File

@@ -1,62 +0,0 @@
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import { ReactNode } from 'react'
interface CommandOption {
value: string
label: string
icon?: ReactNode
onSelect?: () => void
}
interface CommandPaletteProps {
open: boolean
onOpenChange: (open: boolean) => void
placeholder?: string
emptyMessage?: string
groups: {
heading?: string
items: CommandOption[]
}[]
}
export function CommandPalette({
open,
onOpenChange,
placeholder = 'Type a command or search...',
emptyMessage = 'No results found.',
groups,
}: CommandPaletteProps) {
return (
<CommandDialog open={open} onOpenChange={onOpenChange}>
<CommandInput placeholder={placeholder} />
<CommandList>
<CommandEmpty>{emptyMessage}</CommandEmpty>
{groups.map((group, groupIndex) => (
<CommandGroup key={groupIndex} heading={group.heading}>
{group.items.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={() => {
item.onSelect?.()
onOpenChange(false)
}}
>
{item.icon && <span className="mr-2">{item.icon}</span>}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
)
}

View File

@@ -1,38 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { CheckCircle } from '@phosphor-icons/react'
interface CompletionCardProps {
completionScore: number
completionMessage: string
isReadyToExport: boolean
}
export function CompletionCard({
completionScore,
completionMessage,
isReadyToExport
}: CompletionCardProps) {
return (
<Card className="bg-gradient-to-br from-primary/10 to-accent/10 border-primary/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle size={24} weight="duotone" className="text-primary" />
Project Completeness
</CardTitle>
<CardDescription>Overall progress of your application</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-4xl font-bold">{completionScore}%</span>
<Badge variant={isReadyToExport ? 'default' : 'secondary'} className="text-sm">
{isReadyToExport ? 'Ready to Export' : 'In Progress'}
</Badge>
</div>
<Progress value={completionScore} className="h-3" />
<p className="text-sm text-muted-foreground">{completionMessage}</p>
</CardContent>
</Card>
)
}

View File

@@ -1,31 +0,0 @@
import { ComponentDefinition } from '@/lib/component-definitions'
import { Card } from '@/components/ui/card'
import * as Icons from '@phosphor-icons/react'
import { cn } from '@/lib/utils'
interface ComponentPaletteItemProps {
component: ComponentDefinition
onDragStart: (component: ComponentDefinition, e: React.DragEvent) => void
className?: string
}
export function ComponentPaletteItem({ component, onDragStart, className }: ComponentPaletteItemProps) {
const IconComponent = (Icons as any)[component.icon] || Icons.Cube
return (
<Card
draggable
onDragStart={(e) => onDragStart(component, e)}
className={cn(
'p-3 cursor-move hover:bg-accent/50 hover:border-accent transition-all',
'flex flex-col items-center gap-2 text-center',
'hover:scale-105 active:scale-95',
className
)}
>
<IconComponent className="w-6 h-6 text-primary" weight="duotone" />
<span className="text-xs font-medium text-foreground">{component.label}</span>
<span className="text-[10px] text-muted-foreground">{component.type}</span>
</Card>
)
}

View File

@@ -1,101 +0,0 @@
import { UIComponent } from '@/types/json-ui'
import { getComponentDef } from '@/lib/component-definitions'
import { cn } from '@/lib/utils'
import * as Icons from '@phosphor-icons/react'
interface ComponentTreeNodeProps {
component: UIComponent
isSelected: boolean
isHovered: boolean
isDraggedOver: boolean
dropPosition: 'before' | 'after' | 'inside' | null
onSelect: () => void
onHover: () => void
onHoverEnd: () => void
onDragStart: (e: React.DragEvent) => void
onDragOver: (e: React.DragEvent) => void
onDragLeave: (e: React.DragEvent) => void
onDrop: (e: React.DragEvent) => void
depth?: number
hasChildren?: boolean
isExpanded?: boolean
onToggleExpand?: () => void
}
export function ComponentTreeNode({
component,
isSelected,
isHovered,
isDraggedOver,
dropPosition,
onSelect,
onHover,
onHoverEnd,
onDragStart,
onDragOver,
onDragLeave,
onDrop,
depth = 0,
hasChildren = false,
isExpanded = false,
onToggleExpand,
}: ComponentTreeNodeProps) {
const def = getComponentDef(component.type)
const IconComponent = def ? (Icons as any)[def.icon] || Icons.Cube : Icons.Cube
return (
<div className="relative">
{isDraggedOver && dropPosition === 'before' && (
<div className="absolute -top-0.5 left-0 right-0 h-0.5 bg-accent" />
)}
<div
draggable
onDragStart={onDragStart}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
onClick={(e) => {
e.stopPropagation()
onSelect()
}}
onMouseEnter={onHover}
onMouseLeave={onHoverEnd}
style={{ paddingLeft: `${depth * 16}px` }}
className={cn(
'flex items-center gap-2 px-3 py-2 text-sm cursor-pointer',
'hover:bg-muted/50 transition-colors',
'border-l-2 border-transparent',
isSelected && 'bg-accent/20 border-l-accent',
isHovered && !isSelected && 'bg-muted/30',
isDraggedOver && dropPosition === 'inside' && 'bg-primary/10'
)}
>
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation()
onToggleExpand?.()
}}
className="hover:text-accent"
>
{isExpanded ? (
<Icons.CaretDown className="w-3 h-3 text-muted-foreground" />
) : (
<Icons.CaretRight className="w-3 h-3 text-muted-foreground" />
)}
</button>
) : (
<div className="w-3" />
)}
<IconComponent className="w-4 h-4 text-primary" weight="duotone" />
<span className="flex-1 text-foreground truncate">{def?.label || component.type}</span>
<span className="text-xs text-muted-foreground font-mono">{component.id}</span>
</div>
{isDraggedOver && dropPosition === 'after' && (
<div className="absolute -bottom-0.5 left-0 right-0 h-0.5 bg-accent" />
)}
</div>
)
}

View File

@@ -1,34 +0,0 @@
import { Button, ButtonProps } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface ConfirmButtonProps extends Omit<ButtonProps, 'onClick'> {
onConfirm: () => void | Promise<void>
confirmText?: string
isLoading?: boolean
}
export function ConfirmButton({
onConfirm,
confirmText = 'Are you sure?',
isLoading,
children,
className,
...props
}: ConfirmButtonProps) {
const handleClick = async () => {
if (window.confirm(confirmText)) {
await onConfirm()
}
}
return (
<Button
onClick={handleClick}
disabled={isLoading}
className={cn(className)}
{...props}
>
{isLoading ? 'Loading...' : children}
</Button>
)
}

View File

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

View File

@@ -1,73 +0,0 @@
import {
ContextMenu as ShadcnContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
} from '@/components/ui/context-menu'
import { ReactNode } from 'react'
export interface ContextMenuItemType {
label: string
icon?: ReactNode
shortcut?: string
onSelect?: () => void
disabled?: boolean
separator?: boolean
submenu?: ContextMenuItemType[]
}
interface ContextMenuProps {
trigger: ReactNode
items: ContextMenuItemType[]
}
export function ContextMenu({ trigger, items }: ContextMenuProps) {
const renderItems = (menuItems: ContextMenuItemType[]) => {
return menuItems.map((item, index) => {
if (item.separator) {
return <ContextMenuSeparator key={`separator-${index}`} />
}
if (item.submenu && item.submenu.length > 0) {
return (
<ContextMenuSub key={index}>
<ContextMenuSubTrigger>
{item.icon && <span className="mr-2">{item.icon}</span>}
{item.label}
</ContextMenuSubTrigger>
<ContextMenuSubContent>
{renderItems(item.submenu)}
</ContextMenuSubContent>
</ContextMenuSub>
)
}
return (
<ContextMenuItem
key={index}
onSelect={item.onSelect}
disabled={item.disabled}
>
{item.icon && <span className="mr-2">{item.icon}</span>}
<span className="flex-1">{item.label}</span>
{item.shortcut && (
<span className="ml-auto text-xs text-muted-foreground">
{item.shortcut}
</span>
)}
</ContextMenuItem>
)
})
}
return (
<ShadcnContextMenu>
<ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
<ContextMenuContent>{renderItems(items)}</ContextMenuContent>
</ShadcnContextMenu>
)
}

View File

@@ -1,56 +0,0 @@
import { Copy, Check } from '@phosphor-icons/react'
import { useState } from 'react'
import { cn } from '@/lib/utils'
interface CopyButtonProps {
text: string
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function CopyButton({ text, size = 'md', className }: CopyButtonProps) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
const sizeStyles = {
sm: 'p-1',
md: 'p-2',
lg: 'p-3',
}
const iconSize = {
sm: 12,
md: 16,
lg: 20,
}
return (
<button
onClick={handleCopy}
className={cn(
'rounded-md transition-colors',
copied
? 'bg-accent text-accent-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
sizeStyles[size],
className
)}
aria-label={copied ? 'Copied' : 'Copy to clipboard'}
>
{copied ? (
<Check size={iconSize[size]} weight="bold" />
) : (
<Copy size={iconSize[size]} />
)}
</button>
)
}

View File

@@ -1,21 +0,0 @@
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
interface CountBadgeProps {
count: number
max?: number
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
className?: string
}
export function CountBadge({ count, max, variant = 'default', className }: CountBadgeProps) {
const displayValue = max && count > max ? `${max}+` : count.toString()
if (count === 0) return null
return (
<Badge variant={variant} className={cn('ml-2 px-2 py-0.5 text-xs', className)}>
{displayValue}
</Badge>
)
}

View File

@@ -1,36 +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
}
export function DataList({
items,
renderItem,
emptyMessage = 'No items',
className,
itemClassName,
}: DataListProps) {
if (items.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
{emptyMessage}
</div>
)
}
return (
<div className={cn('space-y-2', className)}>
{items.map((item, index) => (
<div key={index} className={cn('transition-colors', itemClassName)}>
{renderItem(item, index)}
</div>
))}
</div>
)
}

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