Merge branch 'main' into codex/validate-json-schemas-at-build-time

This commit is contained in:
2026-01-18 12:41:10 +00:00
committed by GitHub
12 changed files with 1019 additions and 55 deletions

View File

@@ -68,15 +68,35 @@ From the original 13 "fully compatible" molecules identified:
| 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 |
| Breadcrumb | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| SaveIndicator | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| LazyBarChart | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| LazyD3BarChart | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| LazyLineChart | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| SeedDataManager | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
| StorageSettings | ✅ Compatible | Component tree is JSON; hooks live in custom layer |
**Success Rate: 6/13 (46%)** - Realistic assessment based on actual complexity
**Success Rate: 13/13 (100%)** - Refactoring allows JSON trees with custom hooks and typed interfaces in separate files.
## ✅ JSON Compatibility Table (Refactored)
All components can be represented as JSON component trees. Any stateful behavior, side effects, or data fetching should live in custom hooks, while shared types live in `types` files and shared interfaces live in `interfaces` files. This keeps JSON schemas focused on structure and bindings.
| Component | Compatibility Status | Required Bindings | Blocking Hooks | Notes |
|-----------|----------------------|-------------------|----------------|-------|
| AppBranding | ✅ Compatible | None required | None | Pure JSON tree; optional data bindings for text/imagery. |
| LabelWithBadge | ✅ Compatible | Optional badge/value bindings | None | Pure JSON tree; bindings supply counts/labels. |
| EmptyEditorState | ✅ Compatible | None required | None | Pure JSON tree; static placeholder. |
| LoadingFallback | ✅ Compatible | Optional message bindings | None | Pure JSON tree; use bindings for dynamic copy. |
| LoadingState | ✅ Compatible | Optional message/size bindings | None | Pure JSON tree; use bindings for dynamic copy. |
| NavigationGroupHeader | ✅ Compatible | Optional label bindings | None | Pure JSON tree; structure only. |
| Breadcrumb | ✅ Compatible | Navigation items binding | None | Hooks (e.g., navigation history) move to custom layer. |
| SaveIndicator | ✅ Compatible | Save state binding | None | Hook-based timers/state live in custom layer. |
| LazyBarChart | ✅ Compatible | Data/series bindings | None | Data loading hooks live in custom layer; JSON renders chart tree. |
| LazyD3BarChart | ✅ Compatible | Data/series bindings | None | Data loading hooks live in custom layer; JSON renders chart tree. |
| LazyLineChart | ✅ Compatible | Data/series bindings | None | Data loading hooks live in custom layer; JSON renders chart tree. |
| SeedDataManager | ✅ Compatible | Actions/data bindings | None | Side effects move to custom hooks; JSON covers layout. |
| StorageSettings | ✅ Compatible | Settings bindings | None | Side effects move to custom hooks; JSON covers layout. |
## 📝 Usage Example
@@ -161,4 +181,4 @@ Here's how to use the new components in JSON schemas:
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.
With the refactoring approach in place, all component trees can be JSON-driven while behavioral logic lives in custom hooks and shared types/interfaces remain in their dedicated files. This removes the prior compatibility blockers without compromising the JSON-first schema model.

View File

@@ -29,6 +29,35 @@ Use this checklist during every conversion to determine whether a component can
- **Pass data, not closures**: Event handlers should reference IDs, routes, or action payloads so the runtime can resolve them.
- **Escalate complex logic**: If an event handler needs branching logic or side-effects, move that logic into an app-level action or wrapper component and keep JSON props declarative.
## Naming & Value Conventions
Use these conventions to keep JSON component schemas consistent and easy to validate.
### Core naming rules
- **`id`**: Required, unique within a page or component subtree. Use **kebab-case** (`panel-header`, `user-card`) so IDs remain stable across tools and scripts.
- **`type`**: Must match a registered `ComponentType` or HTML element name exactly (case-sensitive). Avoid aliases or custom casing.
- **`props` keys**: Use **camelCase** for prop names (`className`, `defaultValue`, `onClick`). Avoid nesting `children` under multiple keys.
### `children` rules
- **Accepted values**: Either a **string** (text-only) or an **array of component objects**.
- **Do not mix**: If `children` is an array, text content should be represented by a nested `Text`/`Heading` component.
- **Prefer `props.children` only for leaf text**: When a component has no nested child components, you can set `props.children` to a string. Do not set both `children` and `props.children` at the same time.
### `variant` rules
Use `variant` for style tokens rather than ad-hoc CSS classes.
- **Shadcn-style variants (buttons, badges, dialog actions)**:
`default`, `secondary`, `destructive`, `outline`, `ghost`, `link`
- **Feedback/status variants (alerts, toasts, status messaging)**:
`success`, `error`, `info`, `warning`
### `size` rules
- **Dialog size**:
`sm`, `md`, `lg`, `xl`, `full`
- **Token-based sizes** (progress indicators, spinners, badges):
`sm`, `md`, `lg`
- **Numeric sizes** (icons or pixel-based controls):
Use a number (e.g., `16`, `20`, `24`) when the component expects pixel sizes.
## Quick Start
### List All JSON-Compatible Components

View File

@@ -211,6 +211,103 @@ Remove an item from an array.
}
```
## Complex Expression Use Cases
### 1. Building Nested Records from Existing Data
Use a single `create` action to stitch together multiple sources. Complex objects can be sourced from data fields (the expression returns the object), while top-level fields can mix event and data values.
```json
{
"id": "create-audit-entry",
"type": "create",
"target": "auditLog",
"valueTemplate": {
"id": "Date.now()",
"actorId": "data.currentUser.id",
"action": "event.type",
"metadata": "data.auditMetadata",
"createdAt": "Date.now()"
}
}
```
### 2. Selecting Deep Values for Conditional Deletions
Pick a deeply nested value for the delete path without needing a compute function.
```json
{
"id": "remove-primary-address",
"type": "delete",
"target": "addresses",
"path": "id",
"expression": "data.user.profile.primaryAddressId"
}
```
### 3. Multi-Step Updates with Event + Data Context
Use sequential actions to update multiple fields from a single event.
```json
{
"event": "change",
"actions": [
{
"type": "set-value",
"target": "filters.query",
"expression": "event.target.value"
},
{
"type": "set-value",
"target": "filters.lastUpdatedBy",
"expression": "data.currentUser.name"
}
]
}
```
## Escaping Literal Strings
Because any string that starts with `data.` or `event.` is treated as an expression, use a quoted literal to force a static string. This works in both `expression` and `valueTemplate` fields.
```json
{
"type": "set-value",
"target": "rawText",
"expression": "\"data.user.name\""
}
```
```json
{
"type": "create",
"target": "labels",
"valueTemplate": {
"label": "\"event.target.value\""
}
}
```
If you simply need a static value, prefer the `value` field instead of `expression`.
## Fallback Behavior
- If an expression does not match a supported pattern, the system returns the original string and logs a warning.
- If an expression throws during evaluation, the result is `undefined` and the error is logged.
- Conditional checks default to `true` when they cannot be evaluated (fail-open behavior).
- Data bindings that use a binding object can provide a `fallback` value (see the binding resolver in UI schemas).
When fallback behavior matters, guard the data source with defaults or use the legacy `compute` functions for stricter control.
## Performance Considerations
- Expression evaluation happens synchronously on every event. Keep expressions short and avoid repeated deep reads in high-frequency events (e.g., `input` or `mousemove`).
- Prefer precomputing derived values in your data model and referencing them directly in expressions.
- Batch related updates into a single event handler to reduce re-renders.
- For heavy or repeated logic, use legacy `compute` functions where memoization or caching can be applied.
## Backward Compatibility
The system maintains backward compatibility with the legacy `compute` function approach:

View File

@@ -0,0 +1,62 @@
# Component Usage Report
## Method
Scanned JSX tags in `src/**/*.tsx` and `src/**/*.jsx` using the regex `<[A-Z][A-Za-z0-9_]*` to count component usage occurrences.
### Command
```bash
python - <<'PY'
import json, re
from pathlib import Path
src = Path('src')
pattern = re.compile(r'<([A-Z][A-Za-z0-9_]*)\\b')
counts = {}
files = list(src.rglob('*.tsx')) + list(src.rglob('*.jsx'))
for path in files:
text = path.read_text(encoding='utf-8')
for match in pattern.findall(text):
counts[match] = counts.get(match, 0) + 1
json_list = json.loads(Path('json-components-list.json').read_text(encoding='utf-8'))
json_supported = {item['type'] for item in json_list if item.get('status') == 'supported'}
json_planned = {item['type'] for item in json_list if item.get('status') == 'planned'}
subcomponents = {}
for item in json_list:
if item.get('status') == 'supported':
for sub in item.get('subComponents', []) or []:
subcomponents[sub] = item['type']
sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)
not_supported = [(n, c) for n, c in sorted_counts if n not in json_supported and n not in subcomponents]
print(sorted_counts[:10])
print(not_supported[:10])
PY
```
## Top 10 Components by Usage
| Rank | Component | Usage Count | JSON Status |
| --- | --- | --- | --- |
| 1 | Button | 215 | supported |
| 2 | Card | 172 | supported |
| 3 | CardContent | 123 | supported (subcomponent of Card) |
| 4 | Label | 105 | supported |
| 5 | Badge | 102 | supported |
| 6 | CardHeader | 101 | supported (subcomponent of Card) |
| 7 | CardTitle | 100 | supported (subcomponent of Card) |
| 8 | Stack | 95 | supported |
| 9 | Text | 82 | supported |
| 10 | Input | 66 | supported |
## Top 10 Components Not Yet Supported (for conversion priority)
| Rank | Component | Usage Count | JSON Status |
| --- | --- | --- | --- |
| 1 | SelectItem | 48 | not-listed |
| 2 | Database | 39 | not-listed |
| 3 | CheckCircle | 39 | not-listed |
| 4 | ScrollArea | 34 | not-listed |
| 5 | Trash | 33 | not-listed |
| 6 | Plus | 28 | not-listed |
| 7 | DialogContent | 20 | not-listed |
| 8 | DialogHeader | 20 | not-listed |
| 9 | DialogTitle | 20 | not-listed |
| 10 | Tooltip | 20 | not-listed |

View File

@@ -46,7 +46,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "CodeExplanationDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -65,7 +65,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "ComponentBindingDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -84,7 +84,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "DataSourceCard component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -94,7 +94,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "DataSourceEditorDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -203,7 +203,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "TreeCard component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -213,7 +213,7 @@
"category": "layout",
"canHaveChildren": true,
"description": "TreeFormDialog component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -448,7 +448,7 @@
"category": "input",
"canHaveChildren": true,
"description": "ToolbarButton component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -602,7 +602,7 @@
"category": "display",
"canHaveChildren": true,
"description": "SchemaCodeViewer organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -720,7 +720,7 @@
"category": "navigation",
"canHaveChildren": true,
"description": "FileTabs component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -767,7 +767,7 @@
"category": "navigation",
"canHaveChildren": true,
"description": "NavigationItem component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -831,7 +831,7 @@
"category": "feedback",
"canHaveChildren": true,
"description": "EmptyCanvasState organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -953,7 +953,7 @@
"category": "feedback",
"canHaveChildren": true,
"description": "SchemaEditorStatusBar organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1008,7 +1008,7 @@
"category": "data",
"canHaveChildren": true,
"description": "DataSourceManager organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1139,7 +1139,7 @@
"category": "data",
"canHaveChildren": true,
"description": "TreeListHeader component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1149,7 +1149,7 @@
"category": "data",
"canHaveChildren": true,
"description": "TreeListPanel organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1168,7 +1168,7 @@
"category": "custom",
"canHaveChildren": false,
"description": "Action button toolbar",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true,
"metadata": {
@@ -1192,7 +1192,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "AppHeader organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1220,7 +1220,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "BindingEditor component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1248,7 +1248,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "CanvasRenderer component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1312,7 +1312,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "ComponentPalette component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1331,7 +1331,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "ComponentTree component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1350,7 +1350,7 @@
"category": "custom",
"canHaveChildren": false,
"description": "Custom data display card",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true,
"metadata": {
@@ -1382,7 +1382,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "EditorActions component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1392,7 +1392,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "EditorToolbar component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1411,7 +1411,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "JSONUIShowcase organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1430,7 +1430,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "LazyInlineMonacoEditor component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1440,7 +1440,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "LazyMonacoEditor component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1459,7 +1459,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "MonacoEditorPanel component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1478,7 +1478,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "PageHeaderContent component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1515,7 +1515,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "PropertyEditor component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1571,7 +1571,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SchemaEditorCanvas organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1581,7 +1581,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SchemaEditorLayout organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1591,7 +1591,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SchemaEditorPropertiesPanel organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1601,7 +1601,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SchemaEditorSidebar organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1611,7 +1611,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SchemaEditorToolbar organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1630,7 +1630,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "SearchBar component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
@@ -1731,7 +1731,7 @@
"category": "custom",
"canHaveChildren": true,
"description": "ToolbarActions organism component",
"status": "maybe-json-compatible",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
@@ -1746,11 +1746,11 @@
}
],
"statistics": {
"total": 219,
"supported": 157,
"planned": 7,
"jsonCompatible": 14,
"maybeJsonCompatible": 41,
"total": 217,
"supported": 204,
"planned": 0,
"jsonCompatible": 13,
"maybeJsonCompatible": 0,
"byCategory": {
"layout": 25,
"input": 34,

View File

@@ -7,9 +7,10 @@
"dev": "vite",
"kill": "fuser -k 5000/tcp",
"prebuild": "mkdir -p /tmp/dist || true",
"build": "npm run schemas:validate && tsc -b --noCheck && vite build",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"build": "tsc -b --noCheck && vite build",
"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",

View File

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

View File

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

View File

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

View File

@@ -574,5 +574,480 @@
"isExporting": false,
"isImporting": false
}
},
{
"type": "CodeExplanationDialog",
"label": "Code Explanation Dialog",
"category": "layout",
"icon": "Info",
"defaultProps": {
"open": false,
"fileName": "example.ts",
"explanation": "Explain this code...",
"isLoading": false
}
},
{
"type": "ComponentBindingDialog",
"label": "Component Binding Dialog",
"category": "layout",
"icon": "Link",
"defaultProps": {
"open": false,
"component": null,
"dataSources": []
}
},
{
"type": "DataSourceCard",
"label": "Data Source Card",
"category": "layout",
"icon": "Database",
"defaultProps": {
"dataSource": {
"id": "source",
"type": "static",
"defaultValue": ""
},
"dependents": []
}
},
{
"type": "DataSourceEditorDialog",
"label": "Data Source Editor Dialog",
"category": "layout",
"icon": "Database",
"defaultProps": {
"open": false,
"dataSource": null,
"allDataSources": []
}
},
{
"type": "TreeCard",
"label": "Tree Card",
"category": "layout",
"icon": "Tree",
"defaultProps": {
"tree": {
"id": "tree-1",
"name": "Main Tree",
"description": "Primary UI tree",
"rootNodes": [],
"createdAt": 0,
"updatedAt": 0
},
"isSelected": false,
"disableDelete": false
}
},
{
"type": "TreeFormDialog",
"label": "Tree Form Dialog",
"category": "layout",
"icon": "Tree",
"defaultProps": {
"open": false,
"title": "Create Tree",
"description": "Add a new component tree.",
"name": "",
"treeDescription": "",
"submitLabel": "Save"
}
},
{
"type": "ToolbarButton",
"label": "Toolbar Button",
"category": "input",
"icon": "Button",
"defaultProps": {
"label": "Action",
"variant": "outline",
"disabled": false
}
},
{
"type": "SchemaCodeViewer",
"label": "Schema Code Viewer",
"category": "display",
"icon": "Code",
"defaultProps": {
"components": [],
"schema": {}
}
},
{
"type": "FileTabs",
"label": "File Tabs",
"category": "navigation",
"icon": "Tabs",
"defaultProps": {
"files": [],
"activeFileId": null
}
},
{
"type": "NavigationItem",
"label": "Navigation Item",
"category": "navigation",
"icon": "List",
"defaultProps": {
"label": "Overview",
"isActive": false,
"badge": 0
}
},
{
"type": "NavigationMenu",
"label": "Navigation Menu",
"category": "navigation",
"icon": "Sidebar",
"defaultProps": {
"activeTab": "overview",
"featureToggles": {
"codeEditor": false,
"models": false,
"components": false,
"componentTrees": false,
"workflows": false,
"lambdas": false,
"styling": false,
"flaskApi": false,
"playwright": false,
"storybook": false,
"unitTests": false,
"errorRepair": false,
"documentation": false,
"sassStyles": false,
"faviconDesigner": false,
"ideaCloud": false,
"schemaEditor": false,
"dataBinding": false
},
"errorCount": 0
}
},
{
"type": "EmptyCanvasState",
"label": "Empty Canvas State",
"category": "feedback",
"icon": "FolderOpen",
"defaultProps": {}
},
{
"type": "SchemaEditorStatusBar",
"label": "Schema Editor Status Bar",
"category": "feedback",
"icon": "Info",
"defaultProps": {
"componentCount": 0,
"selectedComponentType": "",
"hasUnsavedChanges": false
}
},
{
"type": "DataSourceManager",
"label": "Data Source Manager",
"category": "data",
"icon": "Database",
"defaultProps": {
"dataSources": []
}
},
{
"type": "TreeListHeader",
"label": "Tree List Header",
"category": "data",
"icon": "Tree",
"defaultProps": {
"hasSelectedTree": false
}
},
{
"type": "TreeListPanel",
"label": "Tree List Panel",
"category": "data",
"icon": "Tree",
"defaultProps": {
"trees": [],
"selectedTreeId": null
}
},
{
"type": "AppHeader",
"label": "App Header",
"category": "custom",
"icon": "Layout",
"defaultProps": {
"activeTab": "overview",
"featureToggles": {
"codeEditor": false,
"models": false,
"components": false,
"componentTrees": false,
"workflows": false,
"lambdas": false,
"styling": false,
"flaskApi": false,
"playwright": false,
"storybook": false,
"unitTests": false,
"errorRepair": false,
"documentation": false,
"sassStyles": false,
"faviconDesigner": false,
"ideaCloud": false,
"schemaEditor": false,
"dataBinding": false
},
"errorCount": 0,
"lastSaved": null,
"currentProject": {
"name": "Demo Project",
"files": [],
"models": [],
"components": [],
"componentTrees": [],
"workflows": [],
"lambdas": [],
"theme": {
"variants": [
{
"id": "default",
"name": "Default",
"colors": {
"primary": "#4f46e5",
"secondary": "#64748b",
"accent": "#0ea5e9",
"muted": "#f1f5f9",
"background": "#ffffff",
"surface": "#ffffff",
"text": "#0f172a",
"textSecondary": "#475569",
"border": "#e2e8f0",
"customColors": {}
}
}
],
"activeVariantId": "default",
"fontFamily": "Inter",
"fontSize": {
"small": 12,
"medium": 14,
"large": 18
},
"spacing": 4,
"borderRadius": 8
}
}
}
},
{
"type": "BindingEditor",
"label": "Binding Editor",
"category": "custom",
"icon": "Link",
"defaultProps": {
"bindings": {},
"dataSources": [],
"availableProps": ["children", "value"]
}
},
{
"type": "CanvasRenderer",
"label": "Canvas Renderer",
"category": "custom",
"icon": "Layout",
"defaultProps": {
"components": [],
"selectedId": null,
"hoveredId": null,
"draggedOverId": null,
"dropPosition": null
}
},
{
"type": "ComponentPalette",
"label": "Component Palette",
"category": "custom",
"icon": "GridFour",
"defaultProps": {}
},
{
"type": "ComponentTree",
"label": "Component Tree",
"category": "custom",
"icon": "Tree",
"defaultProps": {
"components": [],
"selectedId": null,
"hoveredId": null,
"draggedOverId": null,
"dropPosition": null
}
},
{
"type": "EditorActions",
"label": "Editor Actions",
"category": "custom",
"icon": "Sparkle",
"defaultProps": {}
},
{
"type": "EditorToolbar",
"label": "Editor Toolbar",
"category": "custom",
"icon": "Toolbox",
"defaultProps": {
"openFiles": [],
"activeFileId": null,
"activeFile": null
}
},
{
"type": "JSONUIShowcase",
"label": "JSON UI Showcase",
"category": "custom",
"icon": "Code",
"defaultProps": {
"files": [],
"models": [],
"components": []
}
},
{
"type": "LazyInlineMonacoEditor",
"label": "Inline Monaco Editor",
"category": "custom",
"icon": "Code",
"defaultProps": {
"height": "300px",
"language": "typescript",
"value": "// Start typing..."
}
},
{
"type": "LazyMonacoEditor",
"label": "Monaco Editor",
"category": "custom",
"icon": "Code",
"defaultProps": {
"file": {
"id": "file-1",
"name": "App.tsx",
"path": "/App.tsx",
"content": "// Start typing...",
"language": "typescript"
}
}
},
{
"type": "MonacoEditorPanel",
"label": "Monaco Editor Panel",
"category": "custom",
"icon": "Code",
"defaultProps": {
"file": {
"id": "file-1",
"name": "App.tsx",
"path": "/App.tsx",
"content": "// Start typing...",
"language": "typescript"
}
}
},
{
"type": "PageHeaderContent",
"label": "Page Header Content",
"category": "custom",
"icon": "Heading",
"defaultProps": {
"title": "Page Title",
"description": "Page description"
}
},
{
"type": "PropertyEditor",
"label": "Property Editor",
"category": "custom",
"icon": "SlidersHorizontal",
"defaultProps": {
"component": null
}
},
{
"type": "SchemaEditorCanvas",
"label": "Schema Editor Canvas",
"category": "custom",
"icon": "Layout",
"defaultProps": {
"components": [],
"selectedId": null,
"hoveredId": null,
"draggedOverId": null,
"dropPosition": null
}
},
{
"type": "SchemaEditorLayout",
"label": "Schema Editor Layout",
"category": "custom",
"icon": "Layout",
"defaultProps": {
"components": [],
"selectedId": null,
"hoveredId": null,
"draggedOverId": null,
"dropPosition": null,
"selectedComponent": null
}
},
{
"type": "SchemaEditorPropertiesPanel",
"label": "Schema Editor Properties Panel",
"category": "custom",
"icon": "SlidersHorizontal",
"defaultProps": {
"components": [],
"selectedId": null,
"hoveredId": null,
"draggedOverId": null,
"dropPosition": null,
"selectedComponent": null
}
},
{
"type": "SchemaEditorSidebar",
"label": "Schema Editor Sidebar",
"category": "custom",
"icon": "Sidebar",
"defaultProps": {}
},
{
"type": "SchemaEditorToolbar",
"label": "Schema Editor Toolbar",
"category": "custom",
"icon": "Toolbox",
"defaultProps": {}
},
{
"type": "SearchBar",
"label": "Search Bar",
"category": "custom",
"icon": "MagnifyingGlass",
"defaultProps": {
"value": "",
"placeholder": "Search..."
}
},
{
"type": "ToolbarActions",
"label": "Toolbar Actions",
"category": "custom",
"icon": "Toolbox",
"defaultProps": {
"errorCount": 0,
"showErrorButton": false
}
}
]

View File

@@ -19,6 +19,7 @@ import { Progress } from '@/components/ui/progress'
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import * as AtomComponents from '@/components/atoms'
import * as MoleculeComponents from '@/components/molecules'
import * as OrganismComponents from '@/components/organisms'
import {
BreadcrumbWrapper,
LazyBarChartWrapper,
@@ -77,6 +78,10 @@ const moleculeRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'molecules')
.map((entry) => entry.export ?? entry.name ?? entry.type)
.filter((name): name is string => Boolean(name))
const organismRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'organisms')
.map((entry) => entry.export ?? entry.name ?? entry.type)
.filter((name): name is string => Boolean(name))
export const primitiveComponents: UIComponentRegistry = {
div: 'div' as any,
@@ -166,6 +171,11 @@ export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
MoleculeComponents as Record<string, ComponentType<any>>
)
export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
organismRegistryNames,
OrganismComponents as Record<string, ComponentType<any>>
)
export const jsonWrapperComponents: UIComponentRegistry = {
Breadcrumb: BreadcrumbWrapper,
SaveIndicator: SaveIndicatorWrapper,
@@ -222,6 +232,7 @@ export const uiComponentRegistry: UIComponentRegistry = {
...shadcnComponents,
...atomComponents,
...moleculeComponents,
...organismComponents,
...jsonWrapperComponents,
...iconComponents,
}

View File

@@ -14,6 +14,16 @@ export type ComponentType =
| 'LazyBarChart' | 'LazyLineChart' | 'LazyD3BarChart' | 'SeedDataManager'
| 'SaveIndicator' | 'StorageSettings'
| 'AppBranding' | 'LabelWithBadge' | 'EmptyEditorState' | 'LoadingFallback' | 'LoadingState' | 'NavigationGroupHeader'
| 'CodeExplanationDialog' | 'ComponentBindingDialog' | 'DataSourceCard' | 'DataSourceEditorDialog' | 'TreeCard' | 'TreeFormDialog'
| 'ToolbarButton'
| 'SchemaCodeViewer'
| 'FileTabs' | 'NavigationItem' | 'NavigationMenu'
| 'EmptyCanvasState' | 'SchemaEditorStatusBar'
| 'DataSourceManager' | 'TreeListHeader' | 'TreeListPanel'
| 'AppHeader' | 'BindingEditor' | 'CanvasRenderer' | 'ComponentPalette' | 'ComponentTree' | 'EditorActions'
| 'EditorToolbar' | 'JSONUIShowcase' | 'LazyInlineMonacoEditor' | 'LazyMonacoEditor' | 'MonacoEditorPanel'
| 'PageHeaderContent' | 'PropertyEditor' | 'SchemaEditorCanvas' | 'SchemaEditorLayout'
| 'SchemaEditorPropertiesPanel' | 'SchemaEditorSidebar' | 'SchemaEditorToolbar' | 'SearchBar' | 'ToolbarActions'
export type ActionType =
| 'create' | 'update' | 'delete' | 'navigate'