mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
Compare commits
109 Commits
codex/crea
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
| b57c4014ca | |||
| a718aca6f5 | |||
| 7be52ffc1e | |||
| 6388880362 | |||
| c6208fafd1 | |||
| 352ceba09f | |||
| 777a4b8277 | |||
| 1b01492891 | |||
| bbcc91dc80 | |||
| 375d3286e8 | |||
| 83864189a5 | |||
| f547d38539 | |||
| 9a9d76865b | |||
| 39e5385925 | |||
| fa3b31c896 | |||
| 0475085300 | |||
| 5080026ef7 | |||
| d2cc3d60a0 | |||
| 67f2c26f10 | |||
| 9ea7c15f5d | |||
| 92e9b02d6d | |||
| 6f01619141 | |||
| f627f6955f | |||
| 3f190f7e5a | |||
| 7d04abb7d9 | |||
| c439bd733e | |||
| 9fb7765c51 | |||
| 968efc7701 | |||
| d05d16b827 | |||
| 31d6334a65 | |||
| f8b9ce6114 | |||
| 156e471f0b | |||
| edbb2f4af0 | |||
| 83b5e51b7e | |||
| 2a4b527485 | |||
| 0a491528f3 | |||
| 3c96b733b2 | |||
| 21ef3d1d3e | |||
| 6df9c0c3dd | |||
| 5f921e6193 | |||
| 571fe3ef2c | |||
| 3a89430b29 | |||
| 37442350cd | |||
| bd6bd97894 | |||
| dc0cb8d873 | |||
| d725045671 | |||
| 2375630d37 | |||
| ec78ec0f9b | |||
| a00a9c4b1d | |||
| 79732ce358 | |||
| c345e892f9 | |||
| a7ce7b0be6 | |||
| 10a7719e49 | |||
| 578b52bb95 | |||
| a283626538 | |||
| 6cc5adf870 | |||
| 6d4775fb5a | |||
| f428263a54 | |||
| 0ce4f6e7a4 | |||
| 3b15b28059 | |||
| 1261c3e44d | |||
| 595aeb4df8 | |||
| c77753ee0a | |||
| a92c95c28a | |||
| 82f572497c | |||
| 7922c14b7b | |||
| 4cf80e6fd8 | |||
| 575944fa0e | |||
| df2c00dd06 | |||
| 0a1fe149d3 | |||
| 727a66218e | |||
| 29621b2765 | |||
| 0acd252ad0 | |||
| 2859d905ed | |||
| cf3f551698 | |||
| 8f8305f95c | |||
| bcd11011ad | |||
| bd9482b6d4 | |||
| 9e80117569 | |||
| c2fc446f1f | |||
| 13192f422e | |||
| 5b54fd3b2a | |||
| eab0c53210 | |||
| 0042d2e2cd | |||
| d952e1e9fc | |||
| 0d13710c09 | |||
| ef08246fc8 | |||
| af58bcb7c2 | |||
| ae183ef80d | |||
| 53fdc3892d | |||
| 3031232ecf | |||
| af03c13934 | |||
| 4529708f76 | |||
| 8945c746cb | |||
| 2190be271f | |||
| e7fc49e53f | |||
| 9448b8327d | |||
| 64c3b5b12b | |||
| 0d82406e5f | |||
| 233dbd2aa1 | |||
| 3fe02ed098 | |||
| e7159916cb | |||
| e41d08d40c | |||
| fc209545c1 | |||
| 28a3851310 | |||
| 8465a9de5a | |||
| d04333e565 | |||
| e210dd8bec | |||
| 39c57e9967 |
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1,192 +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
|
||||
|
||||
## 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
|
||||
@@ -1,322 +0,0 @@
|
||||
# JSON Expression System
|
||||
|
||||
This document describes the JSON-friendly expression system for handling events without requiring external TypeScript functions.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Expression Types
|
||||
|
||||
### 1. Simple Expressions
|
||||
|
||||
Use the `expression` field to evaluate dynamic values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "set-value",
|
||||
"target": "username",
|
||||
"expression": "event.target.value"
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
- **Event Access**: `"event.target.value"`, `"event.key"`, `"event.type"`
|
||||
- Access event properties
|
||||
- Commonly used for form inputs
|
||||
|
||||
- **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:
|
||||
|
||||
```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",
|
||||
"completed": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### update
|
||||
Update an existing value (similar to set-value).
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "update-count",
|
||||
"type": "update",
|
||||
"target": "viewCount",
|
||||
"expression": "data.viewCount + 1"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Arithmetic expressions are not yet supported. Use `increment` action type instead.
|
||||
|
||||
### delete
|
||||
Remove an item from an array.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "remove-todo",
|
||||
"type": "delete",
|
||||
"target": "todos",
|
||||
"path": "id",
|
||||
"expression": "data.selectedId"
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
9
Jenkinsfile
vendored
9
Jenkinsfile
vendored
@@ -68,6 +68,15 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Component Registry Check') {
|
||||
steps {
|
||||
script {
|
||||
nodejs(nodeJSInstallationName: "Node ${NODE_VERSION}") {
|
||||
sh 'npm run components:validate'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
277
MAYBE_JSON_BINDING_REVIEW.md
Normal file
277
MAYBE_JSON_BINDING_REVIEW.md
Normal 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 aren’t 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.”
|
||||
@@ -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",
|
||||
|
||||
262
docs/COMPONENT_CONVERSION_ANALYSIS.md
Normal file
262
docs/COMPONENT_CONVERSION_ANALYSIS.md
Normal 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
471
docs/HYBRID_ARCHITECTURE.md
Normal 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."**
|
||||
@@ -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
388
docs/JSON_ARCHITECTURE.md
Normal 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.
|
||||
102
docs/JSON_COMPONENT_CONVERSION_TASKS.md
Normal file
102
docs/JSON_COMPONENT_CONVERSION_TASKS.md
Normal 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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
9
docs/json-components-tracker.md
Normal file
9
docs/json-components-tracker.md
Normal 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 |
|
||||
16
e2e/visual-regression.spec.ts
Normal file
16
e2e/visual-regression.spec.ts
Normal 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
13
fixtures/dev-qa/README.md
Normal 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`
|
||||
40
fixtures/dev-qa/custom.json
Normal file
40
fixtures/dev-qa/custom.json
Normal 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
46
fixtures/dev-qa/data.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
42
fixtures/dev-qa/display.json
Normal file
42
fixtures/dev-qa/display.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
40
fixtures/dev-qa/feedback.json
Normal file
40
fixtures/dev-qa/feedback.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
45
fixtures/dev-qa/input.json
Normal file
45
fixtures/dev-qa/input.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
67
fixtures/dev-qa/layout.json
Normal file
67
fixtures/dev-qa/layout.json
Normal 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."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
42
fixtures/dev-qa/navigation.json
Normal file
42
fixtures/dev-qa/navigation.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
58
package-lock.json
generated
58
package-lock.json
generated
@@ -89,6 +89,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^17.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.3.1"
|
||||
@@ -5700,6 +5701,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 +6856,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"
|
||||
@@ -7117,6 +7141,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",
|
||||
|
||||
10
package.json
10
package.json
@@ -8,8 +8,9 @@
|
||||
"kill": "fuser -k 5000/tcp",
|
||||
"prebuild": "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 +22,10 @@
|
||||
"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:scan": "node scripts/scan-and-update-registry.cjs",
|
||||
"components:validate": "node scripts/validate-supported-components.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
@@ -106,6 +109,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^17.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.3.1"
|
||||
|
||||
252
scripts/lint-json-ui-schemas.cjs
Normal file
252
scripts/lint-json-ui-schemas.cjs
Normal 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.')
|
||||
297
scripts/validate-json-schemas.ts
Normal file
297
scripts/validate-json-schemas.ts
Normal 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.')
|
||||
182
scripts/validate-supported-components.cjs
Normal file
182
scripts/validate-supported-components.cjs
Normal file
@@ -0,0 +1,182 @@
|
||||
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.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 componentTypesStart = componentTypesContent.indexOf('export type ComponentType')
|
||||
const componentTypesEnd = componentTypesContent.indexOf('export type ActionType')
|
||||
if (componentTypesStart === -1 || componentTypesEnd === -1) {
|
||||
throw new Error('Unable to locate ComponentType union in src/types/json-ui.ts')
|
||||
}
|
||||
const componentTypesBlock = componentTypesContent.slice(componentTypesStart, componentTypesEnd)
|
||||
const componentTypeSet = new Set()
|
||||
const componentTypeRegex = /'([^']+)'/g
|
||||
let match
|
||||
while ((match = componentTypeRegex.exec(componentTypesBlock)) !== 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.')
|
||||
@@ -7,30 +7,9 @@ import { ComponentBindingsCard } from '@/components/data-binding-designer/Compon
|
||||
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)
|
||||
|
||||
9
src/components/JSONConversionShowcase.tsx
Normal file
9
src/components/JSONConversionShowcase.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
25
src/components/JSONSchemaPageLoader.tsx
Normal file
25
src/components/JSONSchemaPageLoader.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { PageRenderer } from '@/lib/json-ui/page-renderer'
|
||||
import { LoadingFallback } from '@/components/molecules'
|
||||
import { useSchemaLoader } from '@/hooks/use-schema-loader'
|
||||
|
||||
interface JSONSchemaPageLoaderProps {
|
||||
schemaPath: string
|
||||
}
|
||||
|
||||
export function JSONSchemaPageLoader({ schemaPath }: JSONSchemaPageLoaderProps) {
|
||||
const { schema, loading, error } = useSchemaLoader(schemaPath)
|
||||
|
||||
if (loading) {
|
||||
return <LoadingFallback message={`Loading ${schemaPath}...`} />
|
||||
}
|
||||
|
||||
if (error || !schema) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<p className="text-destructive">{error || 'Schema not found'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <PageRenderer schema={schema} />
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -3,8 +3,9 @@ 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 listTableTimelineExample from '@/config/ui-examples/list-table-timeline.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'
|
||||
@@ -14,6 +15,7 @@ const exampleConfigs = {
|
||||
dashboard: dashboardExample,
|
||||
form: formExample,
|
||||
table: tableExample,
|
||||
'list-table-timeline': listTableTimelineExample,
|
||||
settings: settingsExample,
|
||||
}
|
||||
|
||||
@@ -21,6 +23,7 @@ const exampleIcons = {
|
||||
ChartBar,
|
||||
ListBullets,
|
||||
Table,
|
||||
Clock,
|
||||
Gear,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -45,11 +45,12 @@ function getCompletionMessage(score: number): string {
|
||||
}
|
||||
|
||||
export function ProjectDashboard(props: ProjectDashboardProps) {
|
||||
const completionSummary = calculateCompletionScore(props)
|
||||
|
||||
return (
|
||||
<JSONPageRenderer
|
||||
schema={dashboardSchema as any}
|
||||
data={props}
|
||||
functions={{ calculateCompletionScore }}
|
||||
data={{ ...props, completionSummary }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,28 +8,28 @@ interface BreadcrumbItem {
|
||||
}
|
||||
|
||||
interface BreadcrumbNavProps {
|
||||
items: BreadcrumbItem[]
|
||||
items?: BreadcrumbItem[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
|
||||
export function BreadcrumbNav({ items = [], className }: BreadcrumbNavProps) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className={cn('flex items-center gap-2', className)}>
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1
|
||||
const linkClassName = cn(
|
||||
'text-sm transition-colors',
|
||||
isLast ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{item.href || item.onClick ? (
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
'text-sm transition-colors',
|
||||
isLast
|
||||
? 'text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{item.href ? (
|
||||
<a href={item.href} onClick={item.onClick} className={linkClassName}>
|
||||
{item.label}
|
||||
</a>
|
||||
) : item.onClick ? (
|
||||
<button onClick={item.onClick} className={linkClassName}>
|
||||
{item.label}
|
||||
</button>
|
||||
) : (
|
||||
@@ -49,3 +49,5 @@ export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const Breadcrumb = BreadcrumbNav
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import * as Icons from '@phosphor-icons/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
import { getComponentDef } from '@/lib/component-definitions'
|
||||
import { getComponentDef } from '@/lib/component-definition-utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import * as Icons from '@phosphor-icons/react'
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
export interface DataListProps {
|
||||
items: any[]
|
||||
renderItem: (item: any, index: number) => ReactNode
|
||||
renderItem?: (item: any, index: number) => ReactNode
|
||||
emptyMessage?: string
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
itemKey?: string
|
||||
}
|
||||
|
||||
export function DataList({
|
||||
@@ -15,6 +16,7 @@ export function DataList({
|
||||
emptyMessage = 'No items',
|
||||
className,
|
||||
itemClassName,
|
||||
itemKey,
|
||||
}: DataListProps) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
@@ -24,11 +26,28 @@ export function DataList({
|
||||
)
|
||||
}
|
||||
|
||||
const renderFallbackItem = (item: any) => {
|
||||
if (itemKey && item && typeof item === 'object') {
|
||||
const value = item[itemKey]
|
||||
if (value !== undefined && value !== null) {
|
||||
return typeof value === 'string' || typeof value === 'number'
|
||||
? value
|
||||
: JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === 'string' || typeof item === 'number') {
|
||||
return item
|
||||
}
|
||||
|
||||
return JSON.stringify(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className={cn('transition-colors', itemClassName)}>
|
||||
{renderItem(item, index)}
|
||||
{renderItem ? renderItem(item, index) : renderFallbackItem(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
import { Database, Function, File } from '@phosphor-icons/react'
|
||||
import { Database, File } from '@phosphor-icons/react'
|
||||
|
||||
interface DataSourceBadgeProps {
|
||||
type: DataSourceType
|
||||
@@ -13,11 +13,6 @@ const dataSourceConfig = {
|
||||
label: 'KV Storage',
|
||||
className: 'bg-accent/20 text-accent border-accent/30'
|
||||
},
|
||||
computed: {
|
||||
icon: Function,
|
||||
label: 'Computed',
|
||||
className: 'bg-primary/20 text-primary border-primary/30'
|
||||
},
|
||||
static: {
|
||||
icon: File,
|
||||
label: 'Static',
|
||||
|
||||
@@ -54,7 +54,7 @@ export { Timestamp } from './Timestamp'
|
||||
export { ScrollArea } from './ScrollArea'
|
||||
|
||||
export { Tag } from './Tag'
|
||||
export { BreadcrumbNav } from './Breadcrumb'
|
||||
export { Breadcrumb, BreadcrumbNav } from './Breadcrumb'
|
||||
export { IconText } from './IconText'
|
||||
export { TextArea } from './TextArea'
|
||||
export { Input } from './Input'
|
||||
@@ -118,4 +118,3 @@ export { MetricDisplay } from './MetricDisplay'
|
||||
export { KeyValue } from './KeyValue'
|
||||
export { EmptyMessage } from './EmptyMessage'
|
||||
export { StepIndicator } from './StepIndicator'
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Progress } from '@/components/ui/progress'
|
||||
import { StatCard } from '@/components/atoms'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getIcon, resolveBinding } from './utils'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
import { evaluateTemplate } from '@/lib/json-ui/expression-evaluator'
|
||||
import { LegacyPageSchema, PageSectionConfig } from './types'
|
||||
|
||||
interface PageSectionRendererProps {
|
||||
@@ -107,8 +109,21 @@ function PageCard({ card, data, functions }: PageCardProps) {
|
||||
const icon = card.icon ? getIcon(card.icon) : null
|
||||
|
||||
if (card.type === 'gradient-card') {
|
||||
const computeFn = functions[card.dataSource?.compute]
|
||||
const computedData = computeFn ? computeFn(data) : {}
|
||||
const dataSource = card.dataSource
|
||||
let computedData: Record<string, any> = {}
|
||||
|
||||
if (dataSource?.expression) {
|
||||
const resolved = evaluateBindingExpression(dataSource.expression, data, {
|
||||
fallback: {},
|
||||
label: `dashboard card (${card.id})`,
|
||||
})
|
||||
computedData = resolved || {}
|
||||
} else if (dataSource?.valueTemplate) {
|
||||
computedData = evaluateTemplate(dataSource.valueTemplate, { data })
|
||||
} else if (dataSource?.compute) {
|
||||
const computeFn = functions[dataSource.compute]
|
||||
computedData = computeFn ? computeFn(data) : {}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={cn('bg-gradient-to-br border-primary/20', card.gradient)}>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as Icons from '@phosphor-icons/react'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
export function resolveBinding(binding: string, data: Record<string, any>): any {
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${binding}`)
|
||||
return func(...Object.values(data))
|
||||
} catch {
|
||||
return binding
|
||||
}
|
||||
return evaluateBindingExpression(binding, data, {
|
||||
fallback: binding,
|
||||
label: 'json-page-renderer binding',
|
||||
})
|
||||
}
|
||||
|
||||
export function getIcon(iconName: string, props?: any) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
import { getUIComponent } from '@/lib/json-ui/component-registry'
|
||||
import { getComponentDef } from '@/lib/component-definitions'
|
||||
import { getComponentDef } from '@/lib/component-definition-utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createElement, ReactNode } from 'react'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentDefinition, getCategoryComponents } from '@/lib/component-definitions'
|
||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||
import { getCategoryComponents } from '@/lib/component-definition-utils'
|
||||
import { ComponentPaletteItem } from '@/components/atoms/ComponentPaletteItem'
|
||||
import { PanelHeader, Stack } from '@/components/atoms'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import { Card, Badge, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
||||
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||
|
||||
interface DataSourceCardProps {
|
||||
dataSource: DataSource
|
||||
dependents?: DataSource[]
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
||||
const getDependencyCount = () => {
|
||||
if (dataSource.type === 'computed') {
|
||||
return dataSource.dependencies?.length || 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export function DataSourceCard({ dataSource, onEdit, onDelete }: DataSourceCardProps) {
|
||||
const renderTypeSpecificInfo = () => {
|
||||
if (dataSource.type === 'kv') {
|
||||
return (
|
||||
@@ -26,19 +18,7 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (dataSource.type === 'computed') {
|
||||
const depCount = getDependencyCount()
|
||||
return (
|
||||
<Flex align="center" gap="sm">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<ArrowsDownUp className="w-3 h-3 mr-1" />
|
||||
{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -56,13 +36,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
|
||||
{renderTypeSpecificInfo()}
|
||||
|
||||
{dependents.length > 0 && (
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<Text variant="caption">
|
||||
Used by {dependents.length} computed {dependents.length === 1 ? 'source' : 'sources'}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
@@ -78,7 +51,6 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
size="sm"
|
||||
onClick={() => onDelete(dataSource.id)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={dependents.length > 0}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -5,15 +5,12 @@ import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
||||
import { ComputedSourceFields } from '@/components/molecules/data-source-editor/ComputedSourceFields'
|
||||
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
||||
import { useDataSourceEditor } from '@/hooks/use-data-source-editor'
|
||||
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||
|
||||
interface DataSourceEditorDialogProps {
|
||||
open: boolean
|
||||
dataSource: DataSource | null
|
||||
allDataSources: DataSource[]
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (dataSource: DataSource) => void
|
||||
}
|
||||
@@ -21,19 +18,13 @@ interface DataSourceEditorDialogProps {
|
||||
export function DataSourceEditorDialog({
|
||||
open,
|
||||
dataSource,
|
||||
allDataSources,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: DataSourceEditorDialogProps) {
|
||||
const {
|
||||
editingSource,
|
||||
updateField,
|
||||
addDependency,
|
||||
removeDependency,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
} = useDataSourceEditor(dataSource, allDataSources)
|
||||
} = useDataSourceEditor(dataSource)
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editingSource) return
|
||||
@@ -81,18 +72,6 @@ export function DataSourceEditorDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingSource.type === 'computed' && (
|
||||
<ComputedSourceFields
|
||||
editingSource={editingSource}
|
||||
availableDeps={availableDeps}
|
||||
selectedDeps={selectedDeps}
|
||||
unselectedDeps={unselectedDeps}
|
||||
copy={dataSourceEditorCopy.computed}
|
||||
onUpdateField={updateField}
|
||||
onAddDependency={addDependency}
|
||||
onRemoveDependency={removeDependency}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { getComponentDef } from '@/lib/component-definitions'
|
||||
import { getComponentDef } from '@/lib/component-definition-utils'
|
||||
import { PropertyEditorEmptyState } from '@/components/molecules/property-editor/PropertyEditorEmptyState'
|
||||
import { propertyEditorConfig } from '@/components/molecules/property-editor/propertyEditorConfig'
|
||||
import { PropertyEditorHeader } from '@/components/molecules/property-editor/PropertyEditorHeader'
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
|
||||
interface ComputedSourceFieldsCopy {
|
||||
computeLabel: string
|
||||
computePlaceholder: string
|
||||
computeHelp: string
|
||||
dependenciesLabel: string
|
||||
availableSourcesLabel: string
|
||||
emptyDependencies: string
|
||||
}
|
||||
|
||||
interface ComputedSourceFieldsProps {
|
||||
editingSource: DataSource
|
||||
availableDeps: DataSource[]
|
||||
selectedDeps: string[]
|
||||
unselectedDeps: DataSource[]
|
||||
copy: ComputedSourceFieldsCopy
|
||||
onUpdateField: <K extends keyof DataSource>(field: K, value: DataSource[K]) => void
|
||||
onAddDependency: (depId: string) => void
|
||||
onRemoveDependency: (depId: string) => void
|
||||
}
|
||||
|
||||
export function ComputedSourceFields({
|
||||
editingSource,
|
||||
availableDeps,
|
||||
selectedDeps,
|
||||
unselectedDeps,
|
||||
copy,
|
||||
onUpdateField,
|
||||
onAddDependency,
|
||||
onRemoveDependency,
|
||||
}: ComputedSourceFieldsProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.computeLabel}</Label>
|
||||
<Textarea
|
||||
value={editingSource.compute?.toString() || ''}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const fn = new Function('data', `return (${e.target.value})`)()
|
||||
onUpdateField('compute', fn)
|
||||
} catch (err) {
|
||||
// Invalid function
|
||||
}
|
||||
}}
|
||||
placeholder={copy.computePlaceholder}
|
||||
className="font-mono text-sm h-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{copy.computeHelp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.dependenciesLabel}</Label>
|
||||
|
||||
{selectedDeps.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-3 bg-muted/30 rounded border border-border">
|
||||
{selectedDeps.map(depId => (
|
||||
<Badge
|
||||
key={depId}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{depId}
|
||||
<button
|
||||
onClick={() => onRemoveDependency(depId)}
|
||||
className="ml-1 hover:text-destructive"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unselectedDeps.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">{copy.availableSourcesLabel}</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{unselectedDeps.map(ds => (
|
||||
<Button
|
||||
key={ds.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onAddDependency(ds.id)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
+ {ds.id}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableDeps.length === 0 && selectedDeps.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{copy.emptyDependencies}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
export { AppBranding } from './AppBranding'
|
||||
export { Breadcrumb } from './Breadcrumb'
|
||||
export { CanvasRenderer } from './CanvasRenderer'
|
||||
export { CodeExplanationDialog } from './CodeExplanationDialog'
|
||||
export { ComponentPalette } from './ComponentPalette'
|
||||
export { ComponentTree } from './ComponentTree'
|
||||
export { EditorActions } from './EditorActions'
|
||||
export { EditorToolbar } from './EditorToolbar'
|
||||
export { EmptyEditorState } from './EmptyEditorState'
|
||||
@@ -18,8 +21,11 @@ export { MonacoEditorPanel } from './MonacoEditorPanel'
|
||||
export { NavigationGroupHeader } from './NavigationGroupHeader'
|
||||
export { NavigationItem } from './NavigationItem'
|
||||
export { PageHeaderContent } from './PageHeaderContent'
|
||||
export { PropertyEditor } from './PropertyEditor'
|
||||
export { SaveIndicator } from './SaveIndicator'
|
||||
export { SeedDataManager } from './SeedDataManager'
|
||||
export { SearchBar } from './SearchBar'
|
||||
export { StatCard } from './StatCard'
|
||||
export { ToolbarButton } from './ToolbarButton'
|
||||
export { TreeCard } from './TreeCard'
|
||||
export { TreeFormDialog } from './TreeFormDialog'
|
||||
@@ -31,4 +37,3 @@ export { DataSourceCard } from './DataSourceCard'
|
||||
export { BindingEditor } from './BindingEditor'
|
||||
export { DataSourceEditorDialog } from './DataSourceEditorDialog'
|
||||
export { ComponentBindingDialog } from './ComponentBindingDialog'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ComponentSchema as ComponentSchemaType } from '@/types/page-schema'
|
||||
import { getUIComponent } from '@/lib/json-ui/component-registry'
|
||||
import { evaluateConditionExpression, evaluateTransformExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
interface ComponentRendererProps {
|
||||
schema: ComponentSchemaType
|
||||
@@ -17,13 +18,10 @@ export function ComponentRenderer({ schema, context, onEvent }: ComponentRendere
|
||||
}
|
||||
|
||||
if (schema.condition) {
|
||||
try {
|
||||
const conditionFn = new Function('context', `return ${schema.condition}`)
|
||||
if (!conditionFn(context)) {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Condition evaluation failed for ${schema.id}:`, error)
|
||||
const conditionMet = evaluateConditionExpression(schema.condition, context, {
|
||||
label: `component condition (${schema.id})`,
|
||||
})
|
||||
if (!conditionMet) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -34,13 +32,10 @@ export function ComponentRenderer({ schema, context, onEvent }: ComponentRendere
|
||||
schema.bindings.forEach(binding => {
|
||||
const value = getNestedValue(context, binding.source)
|
||||
if (binding.transform) {
|
||||
try {
|
||||
const transformFn = new Function('value', 'context', `return ${binding.transform}`)
|
||||
props[binding.target] = transformFn(value, context)
|
||||
} catch (error) {
|
||||
console.error(`Transform failed for ${binding.target}:`, error)
|
||||
props[binding.target] = value
|
||||
}
|
||||
props[binding.target] = evaluateTransformExpression(binding.transform, value, context, {
|
||||
fallback: value,
|
||||
label: `binding transform (${binding.target})`,
|
||||
})
|
||||
} else {
|
||||
props[binding.target] = value
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
|
||||
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
import { Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { Database, FileText } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { EmptyState, Stack } from '@/components/atoms'
|
||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||
@@ -21,7 +21,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
addDataSource,
|
||||
updateDataSource,
|
||||
deleteDataSource,
|
||||
getDependents,
|
||||
} = useDataSourceManager(dataSources)
|
||||
|
||||
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
|
||||
@@ -42,17 +41,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
}
|
||||
|
||||
const handleDeleteSource = (id: string) => {
|
||||
const dependents = getDependents(id)
|
||||
if (dependents.length > 0) {
|
||||
const noun = dependents.length === 1 ? 'source' : 'sources'
|
||||
toast.error(dataSourceManagerCopy.toasts.deleteBlockedTitle, {
|
||||
description: dataSourceManagerCopy.toasts.deleteBlockedDescription
|
||||
.replace('{count}', String(dependents.length))
|
||||
.replace('{noun}', noun),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deleteDataSource(id)
|
||||
onChange(localSources.filter(ds => ds.id !== id))
|
||||
toast.success(dataSourceManagerCopy.toasts.deleted)
|
||||
@@ -66,7 +54,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
|
||||
const groupedSources = {
|
||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||
computed: localSources.filter(ds => ds.type === 'computed'),
|
||||
static: localSources.filter(ds => ds.type === 'static'),
|
||||
}
|
||||
|
||||
@@ -97,7 +84,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
icon={<Database size={16} />}
|
||||
label={dataSourceManagerCopy.groups.kv}
|
||||
dataSources={groupedSources.kv}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
@@ -106,16 +92,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
icon={<FileText size={16} />}
|
||||
label={dataSourceManagerCopy.groups.static}
|
||||
dataSources={groupedSources.static}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
|
||||
<DataSourceGroupSection
|
||||
icon={<Function size={16} />}
|
||||
label={dataSourceManagerCopy.groups.computed}
|
||||
dataSources={groupedSources.computed}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
@@ -127,7 +103,6 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
|
||||
<DataSourceEditorDialog
|
||||
open={dialogOpen}
|
||||
dataSource={editingSource}
|
||||
allDataSources={localSources}
|
||||
onOpenChange={setDialogOpen}
|
||||
onSave={handleSaveSource}
|
||||
/>
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PageRenderer } from '@/lib/schema-renderer'
|
||||
import { useSchemaLoader } from '@/hooks/ui'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Code, FileText, Database } from '@phosphor-icons/react'
|
||||
import dashboardSchema from '@/config/schemas/json-ui-dashboard.json'
|
||||
|
||||
interface JSONUIShowcaseProps {
|
||||
files?: any[]
|
||||
models?: any[]
|
||||
components?: any[]
|
||||
}
|
||||
|
||||
export function JSONUIShowcase({
|
||||
files = [],
|
||||
models = [],
|
||||
components = []
|
||||
}: JSONUIShowcaseProps) {
|
||||
const [showJSON, setShowJSON] = useState(false)
|
||||
const {schema: loadedSchema, loading, error} = useSchemaLoader({
|
||||
schema: dashboardSchema as any
|
||||
})
|
||||
|
||||
const data = {
|
||||
files: files.length > 0 ? files : [
|
||||
{ name: 'App.tsx', type: 'TypeScript' },
|
||||
{ name: 'index.css', type: 'CSS' },
|
||||
{ name: 'schema-renderer.tsx', type: 'TypeScript' },
|
||||
{ name: 'use-data-binding.ts', type: 'Hook' },
|
||||
{ name: 'dashboard.json', type: 'JSON' },
|
||||
],
|
||||
models: models.length > 0 ? models : [
|
||||
{ name: 'User', fields: 5 },
|
||||
{ name: 'Post', fields: 8 },
|
||||
{ name: 'Comment', fields: 4 },
|
||||
],
|
||||
components: components.length > 0 ? components : [
|
||||
{ name: 'Button', type: 'atom' },
|
||||
{ name: 'Card', type: 'molecule' },
|
||||
{ name: 'Dashboard', type: 'organism' },
|
||||
],
|
||||
}
|
||||
|
||||
const functions = {
|
||||
handleClick: () => {
|
||||
console.log('Button clicked from JSON!')
|
||||
},
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-sm text-muted-foreground">Loading schema...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full p-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
Failed to load schema: {error.message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!loadedSchema) {
|
||||
return (
|
||||
<div className="h-full p-6">
|
||||
<Alert>
|
||||
<AlertDescription>No schema loaded</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="p-6 space-y-6">
|
||||
<Card className="bg-gradient-to-br from-primary/10 to-accent/10 border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code size={24} weight="duotone" className="text-primary" />
|
||||
JSON-Driven UI System
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Complete UI rendering from declarative JSON schemas with data bindings and event handlers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => setShowJSON(!showJSON)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<FileText size={16} weight="duotone" className="mr-2" />
|
||||
{showJSON ? 'Hide' : 'Show'} JSON Schema
|
||||
</Button>
|
||||
</div>
|
||||
{showJSON && (
|
||||
<pre className="bg-secondary/50 p-4 rounded-md overflow-auto text-xs font-mono max-h-96">
|
||||
{JSON.stringify(loadedSchema, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Database size={20} weight="duotone" className="text-blue-500" />
|
||||
Schema-Driven
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
UI structure defined in JSON, making it easy to modify without code changes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Code size={20} weight="duotone" className="text-green-500" />
|
||||
Data Bindings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Dynamic expressions in JSON connect UI to application state seamlessly
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<FileText size={20} weight="duotone" className="text-purple-500" />
|
||||
Atomic Design
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Modular components composed from atoms to organisms following best practices
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Rendered from JSON</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
The content below is entirely generated from the JSON schema above, demonstrating data bindings,
|
||||
loops, and component composition.
|
||||
</p>
|
||||
<PageRenderer schema={loadedSchema} data={data} functions={functions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UIComponent, PageSchema } from '@/types/json-ui'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||
import { SchemaEditorToolbar } from './SchemaEditorToolbar'
|
||||
import { SchemaEditorSidebar } from './SchemaEditorSidebar'
|
||||
import { SchemaEditorCanvas } from './SchemaEditorCanvas'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentPalette } from '@/components/molecules/ComponentPalette'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||
|
||||
interface SchemaEditorSidebarProps {
|
||||
onDragStart: (component: ComponentDefinition, e: React.DragEvent) => void
|
||||
|
||||
@@ -7,7 +7,6 @@ interface DataSourceGroupSectionProps {
|
||||
icon: ReactNode
|
||||
label: string
|
||||
dataSources: DataSource[]
|
||||
getDependents: (id: string) => string[]
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
@@ -16,7 +15,6 @@ export function DataSourceGroupSection({
|
||||
icon,
|
||||
label,
|
||||
dataSources,
|
||||
getDependents,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: DataSourceGroupSectionProps) {
|
||||
@@ -37,7 +35,6 @@ export function DataSourceGroupSection({
|
||||
<DataSourceCard
|
||||
key={ds.id}
|
||||
dataSource={ds}
|
||||
dependents={getDependents(ds.id)}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
|
||||
import { Plus, Database, FileText } from '@phosphor-icons/react'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
|
||||
interface DataSourceManagerHeaderCopy {
|
||||
@@ -14,7 +14,6 @@ interface DataSourceManagerHeaderCopy {
|
||||
addLabel: string
|
||||
menu: {
|
||||
kv: string
|
||||
computed: string
|
||||
static: string
|
||||
}
|
||||
}
|
||||
@@ -49,10 +48,6 @@ export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeader
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{copy.menu.kv}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAdd('computed')}>
|
||||
<Function className="w-4 h-4 mr-2" />
|
||||
{copy.menu.computed}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAdd('static')}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{copy.menu.static}
|
||||
|
||||
@@ -2,6 +2,7 @@ export { NavigationMenu } from './NavigationMenu'
|
||||
export { PageHeader } from './PageHeader'
|
||||
export { ToolbarActions } from './ToolbarActions'
|
||||
export { AppHeader } from './AppHeader'
|
||||
export { DataSourceManager } from './DataSourceManager'
|
||||
export { TreeListPanel } from './TreeListPanel'
|
||||
export { SchemaEditorToolbar } from './SchemaEditorToolbar'
|
||||
export { SchemaEditorSidebar } from './SchemaEditorSidebar'
|
||||
@@ -11,3 +12,4 @@ export { SchemaEditorLayout } from './SchemaEditorLayout'
|
||||
export { EmptyCanvasState } from './EmptyCanvasState'
|
||||
export { SchemaEditorStatusBar } from './SchemaEditorStatusBar'
|
||||
export { SchemaCodeViewer } from './SchemaCodeViewer'
|
||||
export { JSONUIShowcase } from '../JSONUIShowcase'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useSchemaEditor } from '@/hooks/ui/use-schema-editor'
|
||||
import { useDragDrop } from '@/hooks/ui/use-drag-drop'
|
||||
import { useJsonExport } from '@/hooks/ui/use-json-export'
|
||||
import { SchemaEditorLayout } from '@/components/organisms'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { ComponentDefinition } from '@/lib/component-definition-types'
|
||||
import { UIComponent, PageSchema } from '@/types/json-ui'
|
||||
import { toast } from 'sonner'
|
||||
import { schemaEditorConfig } from '@/components/schema-editor/schemaEditorConfig'
|
||||
|
||||
@@ -235,6 +235,16 @@
|
||||
"type": "single"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "json-conversion-showcase",
|
||||
"title": "JSON Conversion Showcase",
|
||||
"description": "JSON conversion showcase overview",
|
||||
"icon": "BookOpen",
|
||||
"component": "JSONConversionShowcase",
|
||||
"layout": {
|
||||
"type": "single"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sass",
|
||||
"title": "Sass Styles",
|
||||
|
||||
17
src/config/get-enabled-pages.ts
Normal file
17
src/config/get-enabled-pages.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PageConfig } from '@/types/page-config'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export function getEnabledPages(featureToggles?: FeatureToggles): PageConfig[] {
|
||||
console.log('[CONFIG] 🔍 getEnabledPages called with toggles:', featureToggles)
|
||||
const enabled = pagesConfig.pages.filter(page => {
|
||||
if (!page.enabled) {
|
||||
console.log('[CONFIG] ⏭️ Skipping disabled page:', page.id)
|
||||
return false
|
||||
}
|
||||
if (!page.toggleKey) return true
|
||||
return featureToggles?.[page.toggleKey as keyof FeatureToggles] !== false
|
||||
}).sort((a, b) => a.order - b.order)
|
||||
console.log('[CONFIG] ✅ Enabled pages:', enabled.map(p => p.id).join(', '))
|
||||
return enabled as PageConfig[]
|
||||
}
|
||||
9
src/config/get-page-by-id.ts
Normal file
9
src/config/get-page-by-id.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PageConfig } from '@/types/page-config'
|
||||
|
||||
export function getPageById(id: string): PageConfig | undefined {
|
||||
console.log('[CONFIG] 🔍 getPageById called for:', id)
|
||||
const page = pagesConfig.pages.find(page => page.id === id)
|
||||
console.log('[CONFIG]', page ? '✅ Page found' : '❌ Page not found')
|
||||
return page as PageConfig | undefined
|
||||
}
|
||||
9
src/config/get-page-config.ts
Normal file
9
src/config/get-page-config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PagesConfig } from '@/types/pages-config'
|
||||
|
||||
export function getPageConfig(): PagesConfig {
|
||||
console.log('[CONFIG] 📄 getPageConfig called')
|
||||
const config = pagesConfig as PagesConfig
|
||||
console.log('[CONFIG] ✅ Pages config loaded:', config.pages.length, 'pages')
|
||||
return config
|
||||
}
|
||||
30
src/config/get-page-shortcuts.ts
Normal file
30
src/config/get-page-shortcuts.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
import { getEnabledPages } from './get-enabled-pages'
|
||||
|
||||
export function getPageShortcuts(featureToggles?: FeatureToggles): Array<{
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
description: string
|
||||
action: string
|
||||
}> {
|
||||
console.log('[CONFIG] ⌨️ getPageShortcuts called')
|
||||
const shortcuts = getEnabledPages(featureToggles)
|
||||
.filter(page => page.shortcut)
|
||||
.map(page => {
|
||||
const parts = page.shortcut!.toLowerCase().split('+')
|
||||
const ctrl = parts.includes('ctrl')
|
||||
const shift = parts.includes('shift')
|
||||
const key = parts[parts.length - 1]
|
||||
|
||||
return {
|
||||
key,
|
||||
ctrl,
|
||||
shift,
|
||||
description: `Go to ${page.title}`,
|
||||
action: page.id
|
||||
}
|
||||
})
|
||||
console.log('[CONFIG] ✅ Shortcuts configured:', shortcuts.length)
|
||||
return shortcuts
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { PWASettings } from '@/components/PWASettings'
|
||||
import { FaviconDesigner } from '@/components/FaviconDesigner'
|
||||
import { FeatureIdeaCloud } from '@/components/FeatureIdeaCloud'
|
||||
import { JSONUIShowcase } from '@/components/JSONUIShowcase'
|
||||
import { JSONConversionShowcase } from '@/components/JSONConversionShowcase'
|
||||
|
||||
export const ComponentRegistry: Record<string, ComponentType<any>> = {
|
||||
Button,
|
||||
@@ -61,6 +62,7 @@ export const ComponentRegistry: Record<string, ComponentType<any>> = {
|
||||
FaviconDesigner,
|
||||
FeatureIdeaCloud,
|
||||
JSONUIShowcase,
|
||||
JSONConversionShowcase,
|
||||
}
|
||||
|
||||
export function getComponent(name: string): ComponentType<any> | null {
|
||||
|
||||
@@ -37,13 +37,6 @@ export function useDataSource(source: DataSource) {
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
case 'computed':
|
||||
return {
|
||||
data: source.defaultValue,
|
||||
setData: () => {},
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
data: null,
|
||||
@@ -67,7 +60,7 @@ export function useDataSources(sources: DataSource[]) {
|
||||
|
||||
useEffect(() => {
|
||||
sources.forEach((source) => {
|
||||
if (source.type === 'static' || source.type === 'computed') {
|
||||
if (source.type === 'static') {
|
||||
updateData(source.id, source.defaultValue)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ActionSchema = z.object({
|
||||
|
||||
export const DataSourceSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.enum(['kv', 'api', 'computed', 'static'], { message: 'Invalid data source type' }),
|
||||
type: z.enum(['kv', 'api', 'static'], { message: 'Invalid data source type' }),
|
||||
key: z.string().optional(),
|
||||
endpoint: z.string().optional(),
|
||||
transform: z.string().optional(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pagesConfig from './pages.json'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
|
||||
export interface PropConfig {
|
||||
@@ -23,7 +24,10 @@ export interface PageConfig {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
component: string
|
||||
component?: string
|
||||
type?: 'json' | 'component'
|
||||
schemaPath?: string
|
||||
schema?: PageSchema
|
||||
enabled: boolean
|
||||
isRoot?: boolean
|
||||
toggleKey?: string
|
||||
|
||||
@@ -365,6 +365,15 @@
|
||||
"order": 22,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "json-conversion-showcase",
|
||||
"title": "JSON Conversion Showcase",
|
||||
"icon": "BookOpen",
|
||||
"component": "JSONConversionShowcase",
|
||||
"enabled": true,
|
||||
"order": 22.1,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"id": "schema-editor",
|
||||
"title": "Schema Editor",
|
||||
|
||||
4
src/config/pages/actions/create-action.json
Normal file
4
src/config/pages/actions/create-action.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "create",
|
||||
"target": ""
|
||||
}
|
||||
4
src/config/pages/actions/delete-action.json
Normal file
4
src/config/pages/actions/delete-action.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "delete",
|
||||
"target": ""
|
||||
}
|
||||
6
src/config/pages/actions/navigate-action.json
Normal file
6
src/config/pages/actions/navigate-action.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "navigate",
|
||||
"params": {
|
||||
"to": ""
|
||||
}
|
||||
}
|
||||
4
src/config/pages/actions/update-action.json
Normal file
4
src/config/pages/actions/update-action.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "update",
|
||||
"target": ""
|
||||
}
|
||||
17
src/config/pages/atomic-library-showcase-page.json
Normal file
17
src/config/pages/atomic-library-showcase-page.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./schema/page-schema.json",
|
||||
"id": "atomic-library-showcase-page",
|
||||
"name": "Atomic Library Showcase",
|
||||
"description": "Showcase of atomic design components",
|
||||
"icon": "Atom",
|
||||
"layout": {
|
||||
"$ref": "./layouts/single-column.json"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"id": "atomic-library-showcase-component",
|
||||
"type": "AtomicLibraryShowcase",
|
||||
"props": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
6
src/config/pages/atoms/alert.json
Normal file
6
src/config/pages/atoms/alert.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Alert",
|
||||
"props": {
|
||||
"variant": "default"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/badge.json
Normal file
6
src/config/pages/atoms/badge.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Badge",
|
||||
"props": {
|
||||
"variant": "default"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/button-destructive.json
Normal file
6
src/config/pages/atoms/button-destructive.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "destructive"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/button-outline.json
Normal file
6
src/config/pages/atoms/button-outline.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Button",
|
||||
"props": {
|
||||
"variant": "outline"
|
||||
}
|
||||
}
|
||||
3
src/config/pages/atoms/card-content.json
Normal file
3
src/config/pages/atoms/card-content.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "CardContent"
|
||||
}
|
||||
3
src/config/pages/atoms/card-footer.json
Normal file
3
src/config/pages/atoms/card-footer.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "CardFooter"
|
||||
}
|
||||
3
src/config/pages/atoms/card-header.json
Normal file
3
src/config/pages/atoms/card-header.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "CardHeader"
|
||||
}
|
||||
3
src/config/pages/atoms/card.json
Normal file
3
src/config/pages/atoms/card.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "Card"
|
||||
}
|
||||
6
src/config/pages/atoms/div-flex-col.json
Normal file
6
src/config/pages/atoms/div-flex-col.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex flex-col"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/div-flex.json
Normal file
6
src/config/pages/atoms/div-flex.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "flex"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/div-grid.json
Normal file
6
src/config/pages/atoms/div-grid.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "div",
|
||||
"props": {
|
||||
"className": "grid"
|
||||
}
|
||||
}
|
||||
7
src/config/pages/atoms/heading-1.json
Normal file
7
src/config/pages/atoms/heading-1.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"level": 1,
|
||||
"className": "text-3xl font-bold"
|
||||
}
|
||||
}
|
||||
7
src/config/pages/atoms/heading-2.json
Normal file
7
src/config/pages/atoms/heading-2.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "Heading",
|
||||
"props": {
|
||||
"level": 2,
|
||||
"className": "text-2xl font-semibold"
|
||||
}
|
||||
}
|
||||
7
src/config/pages/atoms/icon-base.json
Normal file
7
src/config/pages/atoms/icon-base.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "icon-base",
|
||||
"type": "Icon",
|
||||
"props": {
|
||||
"size": 20
|
||||
}
|
||||
}
|
||||
7
src/config/pages/atoms/icon-folder.json
Normal file
7
src/config/pages/atoms/icon-folder.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "icon-folder",
|
||||
"$ref": "./icon-base.json",
|
||||
"props": {
|
||||
"name": "Folder"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/input-text.json
Normal file
6
src/config/pages/atoms/input-text.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Input",
|
||||
"props": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
3
src/config/pages/atoms/label.json
Normal file
3
src/config/pages/atoms/label.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "Label"
|
||||
}
|
||||
7
src/config/pages/atoms/loading-spinner.json
Normal file
7
src/config/pages/atoms/loading-spinner.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "loading-spinner",
|
||||
"type": "LoadingSpinner",
|
||||
"props": {
|
||||
"size": "md"
|
||||
}
|
||||
}
|
||||
3
src/config/pages/atoms/section.json
Normal file
3
src/config/pages/atoms/section.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "section"
|
||||
}
|
||||
6
src/config/pages/atoms/separator.json
Normal file
6
src/config/pages/atoms/separator.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Separator",
|
||||
"props": {
|
||||
"className": "my-4"
|
||||
}
|
||||
}
|
||||
6
src/config/pages/atoms/text-muted.json
Normal file
6
src/config/pages/atoms/text-muted.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
}
|
||||
}
|
||||
7
src/config/pages/atoms/text-small.json
Normal file
7
src/config/pages/atoms/text-small.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "text-small",
|
||||
"type": "Text",
|
||||
"props": {
|
||||
"variant": "small"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user