diff --git a/JSON_COMPATIBILITY_IMPLEMENTATION.md b/JSON_COMPATIBILITY_IMPLEMENTATION.md index 229d689..68469ea 100644 --- a/JSON_COMPATIBILITY_IMPLEMENTATION.md +++ b/JSON_COMPATIBILITY_IMPLEMENTATION.md @@ -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. diff --git a/JSON_COMPONENTS.md b/JSON_COMPONENTS.md index 03ae1d0..303209e 100644 --- a/JSON_COMPONENTS.md +++ b/JSON_COMPONENTS.md @@ -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 diff --git a/JSON_EXPRESSION_SYSTEM.md b/JSON_EXPRESSION_SYSTEM.md index 8ed5c29..8fd9d9a 100644 --- a/JSON_EXPRESSION_SYSTEM.md +++ b/JSON_EXPRESSION_SYSTEM.md @@ -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: diff --git a/docs/component-usage-report.md b/docs/component-usage-report.md new file mode 100644 index 0000000..905344b --- /dev/null +++ b/docs/component-usage-report.md @@ -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 | diff --git a/json-components-registry.json b/json-components-registry.json index 94efde8..2b8cfbf 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -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, diff --git a/package.json b/package.json index ff91f79..9d34649 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/lint-json-ui-schemas.cjs b/scripts/lint-json-ui-schemas.cjs new file mode 100644 index 0000000..2e13c03 --- /dev/null +++ b/scripts/lint-json-ui-schemas.cjs @@ -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.') diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index f04b720..4506686 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -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' - diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 17abb7a..f4a4e6f 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -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' diff --git a/src/lib/component-definitions.json b/src/lib/component-definitions.json index 1f1d22b..9731f20 100644 --- a/src/lib/component-definitions.json +++ b/src/lib/component-definitions.json @@ -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 + } } ] diff --git a/src/lib/json-ui/component-registry.ts b/src/lib/json-ui/component-registry.ts index 2912d38..e36bb03 100644 --- a/src/lib/json-ui/component-registry.ts +++ b/src/lib/json-ui/component-registry.ts @@ -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> ) +export const organismComponents: UIComponentRegistry = buildRegistryFromNames( + organismRegistryNames, + OrganismComponents as Record> +) + export const jsonWrapperComponents: UIComponentRegistry = { Breadcrumb: BreadcrumbWrapper, SaveIndicator: SaveIndicatorWrapper, @@ -222,6 +232,7 @@ export const uiComponentRegistry: UIComponentRegistry = { ...shadcnComponents, ...atomComponents, ...moleculeComponents, + ...organismComponents, ...jsonWrapperComponents, ...iconComponents, } diff --git a/src/types/json-ui.ts b/src/types/json-ui.ts index a9aed51..51c6c9b 100644 --- a/src/types/json-ui.ts +++ b/src/types/json-ui.ts @@ -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'