Merge branch 'main' into codex/add-component-types-and-update-registry

This commit is contained in:
2026-01-18 12:37:05 +00:00
committed by GitHub
12 changed files with 992 additions and 300 deletions

View File

@@ -10,6 +10,54 @@ The JSON UI system allows you to define user interfaces using JSON schemas inste
- Configuration-driven UIs
- Rapid prototyping
## JSON Eligibility Checklist
Use this checklist during every conversion to determine whether a component can be safely represented in JSON.
### Core requirements (must all be true)
- **No side-effects**: The component should be render-only; it must not perform data fetching, subscriptions, timers, or imperative DOM work. Side-effects belong in wrappers or higher-level orchestration.
- **Supported bindings only**: Props must map to supported JSON bindings (static values, expression bindings, or schema-recognized props). Avoid custom callbacks, ref usage, or passing functions through JSON.
- **Stable props**: Props should be serializable and deterministic (strings, numbers, booleans, arrays, objects). Avoid non-serializable values like class instances, functions, or dates unless explicitly supported by the JSON schema.
### Derived state guidance
- **Prefer pure derivation**: If state can be derived from props or data bindings, compute it at render time in the renderer or via expression bindings rather than introducing component state.
- **Avoid local state in JSON components**: If a component needs internal state to function, wrap it in a React component and expose only the minimal, declarative props to JSON.
- **Keep calculations explicit**: Use named props (e.g., `selectedCount`, `isComplete`) rather than embedding complex logic that would need component state.
### Event handler guidance
- **Use schema-recognized actions**: Wire events to supported JSON actions or expression handlers (e.g., `onClick` -> `actions.navigate`, `actions.submit`), not raw functions.
- **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
@@ -150,8 +198,16 @@ Domain-specific components:
## Current Status
- **Total Components**: 60
- **Supported**: 46 (77%)
- **Planned**: 14 (23%)
- **Supported**: 51 (85%)
- **Planned**: 9 (15%)
## Status Summary
| Status | Count |
| --- | ---: |
| Supported | 51 |
| Maybe | 0 |
| Planned | 9 |
## Files

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:

9
Jenkinsfile vendored
View File

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

View File

@@ -0,0 +1,275 @@
# 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": "preview" }] },
"onClear": { "actions": [{ "id": "clear-schema", "type": "set-value", "target": "schema.components", "value": [] }] }
}
}
```
**Why:** these are pure event triggers; `custom` actions cover app-specific flows that arent part of the built-in action types.
### 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": "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.”

View File

@@ -2,6 +2,13 @@
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

View File

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

View File

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

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

View File

@@ -2,7 +2,7 @@
"$schema": "./schemas/json-components-registry-schema.json",
"version": "2.0.0",
"description": "Registry of all components in the application",
"lastUpdated": "2026-01-18T12:05:00.000Z",
"lastUpdated": "2026-01-18T12:20:47.049Z",
"categories": {
"layout": "Layout and container components",
"input": "Form inputs and interactive controls",
@@ -40,15 +40,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Card",
"name": "Card",
"category": "layout",
"canHaveChildren": true,
"description": "Container card with optional header, content, and footer",
"status": "supported",
"source": "ui"
},
{
"type": "CodeExplanationDialog",
"name": "CodeExplanationDialog",
@@ -125,15 +116,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Drawer",
"name": "Drawer",
"category": "layout",
"canHaveChildren": true,
"description": "Sliding panel overlay",
"status": "supported",
"source": "ui"
},
{
"type": "Flex",
"name": "Flex",
@@ -170,15 +152,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "HoverCard",
"name": "HoverCard",
"category": "layout",
"canHaveChildren": true,
"description": "Card shown on hover",
"status": "supported",
"source": "ui"
},
{
"type": "Modal",
"name": "Modal",
@@ -262,15 +235,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Button",
"name": "Button",
"category": "input",
"canHaveChildren": true,
"description": "Interactive button element",
"status": "supported",
"source": "ui"
},
{
"type": "ButtonGroup",
"name": "ButtonGroup",
@@ -289,15 +253,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Checkbox",
"name": "Checkbox",
"category": "input",
"canHaveChildren": false,
"description": "Checkbox toggle control",
"status": "supported",
"source": "ui"
},
{
"type": "ConfirmButton",
"name": "ConfirmButton",
@@ -352,15 +307,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Form",
"name": "Form",
"category": "input",
"canHaveChildren": true,
"description": "Form container component",
"status": "supported",
"source": "ui"
},
{
"type": "IconButton",
"name": "IconButton",
@@ -379,15 +325,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Input",
"name": "Input",
"category": "input",
"canHaveChildren": false,
"description": "Text input field",
"status": "supported",
"source": "ui"
},
{
"type": "InputOtp",
"name": "InputOtp",
@@ -460,15 +397,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Select",
"name": "Select",
"category": "input",
"canHaveChildren": false,
"description": "Dropdown select control",
"status": "supported",
"source": "ui"
},
{
"type": "Slider",
"name": "Slider",
@@ -478,24 +406,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Slider",
"name": "Slider",
"category": "input",
"canHaveChildren": false,
"description": "Numeric range slider",
"status": "supported",
"source": "ui"
},
{
"type": "Switch",
"name": "Switch",
"category": "input",
"canHaveChildren": false,
"description": "Toggle switch control",
"status": "supported",
"source": "atoms"
},
{
"type": "Switch",
"name": "Switch",
@@ -503,7 +413,7 @@
"canHaveChildren": false,
"description": "Toggle switch control",
"status": "supported",
"source": "ui"
"source": "atoms"
},
{
"type": "TextArea",
@@ -523,15 +433,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Toggle",
"name": "Toggle",
"category": "input",
"canHaveChildren": true,
"description": "Toggle button control",
"status": "supported",
"source": "ui"
},
{
"type": "ToggleGroup",
"name": "ToggleGroup",
@@ -569,15 +470,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Avatar",
"name": "Avatar",
"category": "display",
"canHaveChildren": false,
"description": "User avatar image",
"status": "supported",
"source": "ui"
},
{
"type": "AvatarGroup",
"name": "AvatarGroup",
@@ -596,15 +488,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Badge",
"name": "Badge",
"category": "display",
"canHaveChildren": true,
"description": "Small status or count indicator",
"status": "supported",
"source": "ui"
},
{
"type": "CircularProgress",
"name": "CircularProgress",
@@ -695,15 +578,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Label",
"name": "Label",
"category": "display",
"canHaveChildren": true,
"description": "Form label element",
"status": "supported",
"source": "ui"
},
{
"type": "Progress",
"name": "Progress",
@@ -741,15 +615,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Separator",
"name": "Separator",
"category": "display",
"canHaveChildren": false,
"description": "Visual divider line",
"status": "supported",
"source": "ui"
},
{
"type": "Skeleton",
"name": "Skeleton",
@@ -759,15 +624,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Skeleton",
"name": "Skeleton",
"category": "display",
"canHaveChildren": false,
"description": "Loading skeleton placeholder",
"status": "supported",
"source": "ui"
},
{
"type": "Spinner",
"name": "Spinner",
@@ -849,15 +705,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "ContextMenu",
"name": "ContextMenu",
"category": "navigation",
"canHaveChildren": true,
"description": "Right-click context menu",
"status": "supported",
"source": "ui"
},
{
"type": "DropdownMenu",
"name": "DropdownMenu",
@@ -924,16 +771,6 @@
"source": "molecules",
"jsonCompatible": true
},
{
"type": "NavigationMenu",
"name": "NavigationMenu",
"category": "navigation",
"canHaveChildren": true,
"description": "NavigationMenu component",
"status": "supported",
"source": "organisms",
"jsonCompatible": true
},
{
"type": "NavigationMenu",
"name": "NavigationMenu",
@@ -961,15 +798,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Tabs",
"name": "Tabs",
"category": "navigation",
"canHaveChildren": true,
"description": "Tabbed interface container",
"status": "supported",
"source": "ui"
},
{
"type": "Alert",
"name": "Alert",
@@ -979,15 +807,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Alert",
"name": "Alert",
"category": "feedback",
"canHaveChildren": true,
"description": "Alert notification message",
"status": "supported",
"source": "ui"
},
{
"type": "CountBadge",
"name": "CountBadge",
@@ -1044,16 +863,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "EmptyState",
"name": "EmptyState",
"category": "feedback",
"canHaveChildren": true,
"description": "Empty state placeholder",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
{
"type": "EmptyStateIcon",
"name": "EmptyStateIcon",
@@ -1129,16 +938,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "LoadingState",
"name": "LoadingState",
"category": "feedback",
"canHaveChildren": true,
"description": "LoadingState component",
"status": "json-compatible",
"source": "molecules",
"jsonCompatible": true
},
{
"type": "Notification",
"name": "Notification",
@@ -1316,16 +1115,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "StatCard",
"name": "StatCard",
"category": "data",
"canHaveChildren": false,
"description": "Statistic card display",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
{
"type": "Table",
"name": "Table",
@@ -1335,15 +1124,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Table",
"name": "Table",
"category": "data",
"canHaveChildren": false,
"description": "Data table",
"status": "supported",
"source": "ui"
},
{
"type": "Timeline",
"name": "Timeline",
@@ -1382,15 +1162,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Accordion",
"name": "Accordion",
"category": "custom",
"canHaveChildren": true,
"description": "Collapsible content sections",
"status": "supported",
"source": "ui"
},
{
"type": "ActionBar",
"name": "ActionBar",
@@ -1399,7 +1170,11 @@
"description": "Action button toolbar",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
"jsonCompatible": true,
"metadata": {
"conversionPriority": "high",
"notes": "JSON-ready: stateless layout molecule; bindings/events supported in json-ui component types."
}
},
{
"type": "AppBranding",
@@ -1467,15 +1242,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Calendar",
"name": "Calendar",
"category": "custom",
"canHaveChildren": true,
"description": "Calendar date selector",
"status": "supported",
"source": "ui"
},
{
"type": "CanvasRenderer",
"name": "CanvasRenderer",
@@ -1586,7 +1352,11 @@
"description": "Custom data display card",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
"jsonCompatible": true,
"metadata": {
"conversionPriority": "high",
"notes": "JSON-ready: presentational molecule with conditional rendering only; bindings supported in json-ui component types."
}
},
{
"type": "DetailRow",
@@ -1702,16 +1472,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "PageHeader",
"name": "PageHeader",
"category": "custom",
"canHaveChildren": true,
"description": "PageHeader component",
"status": "json-compatible",
"source": "organisms",
"jsonCompatible": true
},
{
"type": "PageHeaderContent",
"name": "PageHeaderContent",
@@ -1749,15 +1509,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "Popover",
"name": "Popover",
"category": "custom",
"canHaveChildren": true,
"description": "Popover overlay content",
"status": "supported",
"source": "ui"
},
{
"type": "PropertyEditor",
"name": "PropertyEditor",
@@ -1873,15 +1624,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "ScrollArea",
"name": "ScrollArea",
"category": "custom",
"canHaveChildren": true,
"description": "Scrollable container area",
"status": "supported",
"source": "ui"
},
{
"type": "SearchBar",
"name": "SearchBar",
@@ -1901,16 +1643,6 @@
"status": "supported",
"source": "atoms"
},
{
"type": "SearchInput",
"name": "SearchInput",
"category": "custom",
"canHaveChildren": false,
"description": "Search input with icon",
"status": "supported",
"source": "molecules",
"jsonCompatible": true
},
{
"type": "Sheet",
"name": "Sheet",
@@ -2011,15 +1743,6 @@
"description": "Tooltip overlay text",
"status": "supported",
"source": "atoms"
},
{
"type": "Tooltip",
"name": "Tooltip",
"category": "custom",
"canHaveChildren": true,
"description": "Tooltip overlay text",
"status": "supported",
"source": "ui"
}
],
"statistics": {

View File

@@ -22,7 +22,8 @@
"pages:validate": "tsx src/config/validate-config.ts",
"pages:generate": "node scripts/generate-page.js",
"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",

View 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.')

View File

@@ -181,29 +181,28 @@
]
},
{
"id": "loading-states-card",
"id": "loading-fallback-card",
"type": "Card",
"children": [
{
"id": "loading-states-header",
"id": "loading-fallback-header",
"type": "CardHeader",
"children": [
{
"id": "loading-states-title",
"id": "loading-fallback-title",
"type": "CardTitle",
"props": { "children": "Loading States" }
"props": { "children": "LoadingFallback" }
},
{
"id": "loading-states-description",
"id": "loading-fallback-description",
"type": "CardDescription",
"props": { "children": "LoadingFallback and LoadingState components" }
"props": { "children": "Inline loading message with spinner" }
}
]
},
{
"id": "loading-states-content",
"id": "loading-fallback-content",
"type": "CardContent",
"props": { "className": "space-y-4" },
"children": [
{
"id": "loading-fallback-wrapper",
@@ -218,7 +217,36 @@
}
}
]
}
]
}
]
},
{
"id": "loading-state-card",
"type": "Card",
"children": [
{
"id": "loading-state-header",
"type": "CardHeader",
"children": [
{
"id": "loading-state-title",
"type": "CardTitle",
"props": { "children": "LoadingState" }
},
{
"id": "loading-state-description",
"type": "CardDescription",
"props": { "children": "Standalone loading state with size variants" }
}
]
},
{
"id": "loading-state-content",
"type": "CardContent",
"props": { "className": "space-y-3" },
"children": [
{
"id": "loading-state-demo",
"type": "LoadingState",
@@ -226,6 +254,14 @@
"message": "Processing request...",
"size": "sm"
}
},
{
"id": "loading-state-demo-lg",
"type": "LoadingState",
"props": {
"message": "Syncing workspace...",
"size": "lg"
}
}
]
}