From bcd11011ade50d23f5b5c639e54521a2ca2cde54 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:22:34 +0000 Subject: [PATCH 01/18] Normalize json components registry --- docs/JSON_COMPONENT_CONVERSION_TASKS.md | 7 + json-components-registry.json | 289 +----------------------- 2 files changed, 9 insertions(+), 287 deletions(-) diff --git a/docs/JSON_COMPONENT_CONVERSION_TASKS.md b/docs/JSON_COMPONENT_CONVERSION_TASKS.md index f312ed4..c304dc4 100644 --- a/docs/JSON_COMPONENT_CONVERSION_TASKS.md +++ b/docs/JSON_COMPONENT_CONVERSION_TASKS.md @@ -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 diff --git a/json-components-registry.json b/json-components-registry.json index 78ef6c5..911370c 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -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": "maybe-json-compatible", - "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": "maybe-json-compatible", - "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": "maybe-json-compatible", - "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", @@ -1467,15 +1238,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", @@ -1702,16 +1464,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 +1501,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 +1616,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 +1635,6 @@ "status": "supported", "source": "atoms" }, - { - "type": "SearchInput", - "name": "SearchInput", - "category": "custom", - "canHaveChildren": false, - "description": "Search input with icon", - "status": "maybe-json-compatible", - "source": "molecules", - "jsonCompatible": true - }, { "type": "Sheet", "name": "Sheet", @@ -2011,15 +1735,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": { From cf3f5516986516289f63d0e470d6d7ed2bb77538 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:23:03 +0000 Subject: [PATCH 02/18] Add JSON eligibility checklist --- JSON_COMPONENTS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/JSON_COMPONENTS.md b/JSON_COMPONENTS.md index f2e943e..b4961ae 100644 --- a/JSON_COMPONENTS.md +++ b/JSON_COMPONENTS.md @@ -10,6 +10,25 @@ 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. + ## Quick Start ### List All JSON-Compatible Components From 0acd252ad000ab47d0256344686d2fd165268ff7 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:23:46 +0000 Subject: [PATCH 03/18] Add component schema templates to JSON UI guide --- docs/JSON_UI_GUIDE.md | 235 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/docs/JSON_UI_GUIDE.md b/docs/JSON_UI_GUIDE.md index de33a1e..272034f 100644 --- a/docs/JSON_UI_GUIDE.md +++ b/docs/JSON_UI_GUIDE.md @@ -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 From 727a66218e0737012e4bfa065e709ef89164cee5 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:24:25 +0000 Subject: [PATCH 04/18] Prioritize JSON-ready molecules in registry --- json-components-registry.json | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/json-components-registry.json b/json-components-registry.json index 6794fef..8ed9958 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -1052,7 +1052,11 @@ "description": "Empty state placeholder", "status": "maybe-json-compatible", "source": "molecules", - "jsonCompatible": true + "jsonCompatible": true, + "metadata": { + "conversionPriority": "high", + "notes": "JSON-ready: presentational molecule without hooks/side effects; bindings supported in json-ui component types." + } }, { "type": "EmptyStateIcon", @@ -1324,7 +1328,11 @@ "description": "Statistic card display", "status": "maybe-json-compatible", "source": "molecules", - "jsonCompatible": true + "jsonCompatible": true, + "metadata": { + "conversionPriority": "high", + "notes": "JSON-ready: pure props-driven display molecule; bindings supported in json-ui component types." + } }, { "type": "Table", @@ -1399,7 +1407,11 @@ "description": "Action button toolbar", "status": "maybe-json-compatible", "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", @@ -1586,7 +1598,11 @@ "description": "Custom data display card", "status": "maybe-json-compatible", "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", @@ -1909,7 +1925,11 @@ "description": "Search input with icon", "status": "maybe-json-compatible", "source": "molecules", - "jsonCompatible": true + "jsonCompatible": true, + "metadata": { + "conversionPriority": "high", + "notes": "JSON-ready: controlled input component; event bindings supported in json-ui component types." + } }, { "type": "Sheet", From 575944fa0ebb2806b65c2a0d0b03658864e2b0fc Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:25:57 +0000 Subject: [PATCH 05/18] Add JSON components tracker and status summary --- JSON_COMPONENTS.md | 12 ++++++++++-- docs/json-components-tracker.md | 9 +++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 docs/json-components-tracker.md diff --git a/JSON_COMPONENTS.md b/JSON_COMPONENTS.md index f2e943e..d73b811 100644 --- a/JSON_COMPONENTS.md +++ b/JSON_COMPONENTS.md @@ -150,8 +150,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 diff --git a/docs/json-components-tracker.md b/docs/json-components-tracker.md new file mode 100644 index 0000000..06d0718 --- /dev/null +++ b/docs/json-components-tracker.md @@ -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 | From 7922c14b7b2b3bf0d66f3e146aa9b95ca9b1dc93 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:26:45 +0000 Subject: [PATCH 06/18] Document JSON binding gaps for maybe-compatible components --- MAYBE_JSON_BINDING_REVIEW.md | 275 +++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 MAYBE_JSON_BINDING_REVIEW.md diff --git a/MAYBE_JSON_BINDING_REVIEW.md b/MAYBE_JSON_BINDING_REVIEW.md new file mode 100644 index 0000000..2973416 --- /dev/null +++ b/MAYBE_JSON_BINDING_REVIEW.md @@ -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 aren’t 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.” From a92c95c28ac061386c8107b88acbafec412de186 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:27:20 +0000 Subject: [PATCH 07/18] Add supported component validation check --- Jenkinsfile | 9 ++ package.json | 3 +- scripts/validate-supported-components.cjs | 182 ++++++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-supported-components.cjs diff --git a/Jenkinsfile b/Jenkinsfile index d0a0486..f77d5b1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,6 +68,15 @@ pipeline { } } } + stage('Component Registry Check') { + steps { + script { + nodejs(nodeJSInstallationName: "Node ${NODE_VERSION}") { + sh 'npm run components:validate' + } + } + } + } } } diff --git a/package.json b/package.json index 1ce366c..67726b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/validate-supported-components.cjs b/scripts/validate-supported-components.cjs new file mode 100644 index 0000000..6a4d643 --- /dev/null +++ b/scripts/validate-supported-components.cjs @@ -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.') From 595aeb4df8581067d572ad4d85a2fe847f7d401d Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:27:50 +0000 Subject: [PATCH 08/18] Expand new molecules showcase examples --- src/schemas/new-molecules-showcase.json | 52 +++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/schemas/new-molecules-showcase.json b/src/schemas/new-molecules-showcase.json index 53beb9e..16bcd81 100644 --- a/src/schemas/new-molecules-showcase.json +++ b/src/schemas/new-molecules-showcase.json @@ -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" + } } ] } From 3b15b280598c47305b00cefc455912d66cbb7467 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:30:40 +0000 Subject: [PATCH 09/18] Add guidance for JSON expression usage --- JSON_EXPRESSION_SYSTEM.md | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) 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: From f428263a54c2468d9abdd2ddfa1e5fe6a5dd3cdf Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:32:07 +0000 Subject: [PATCH 10/18] docs: add JSON component naming conventions --- JSON_COMPONENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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 From 6d4775fb5a5a48e69af41dc7b564e8ba0aa0ff6b Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:32:39 +0000 Subject: [PATCH 11/18] Add component usage report --- docs/component-usage-report.md | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/component-usage-report.md 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 | From 578b52bb9522480744f15ae40bcef32fb09026b0 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:36:11 +0000 Subject: [PATCH 12/18] Add converted JSON UI components --- json-components-registry.json | 92 ++--- src/components/molecules/index.ts | 7 +- src/components/organisms/index.ts | 2 + src/lib/component-definitions.json | 475 ++++++++++++++++++++++++++ src/lib/json-ui/component-registry.ts | 11 + src/types/json-ui.ts | 10 + 6 files changed, 550 insertions(+), 47 deletions(-) diff --git a/json-components-registry.json b/json-components-registry.json index 6794fef..0471990 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -55,7 +55,7 @@ "category": "layout", "canHaveChildren": true, "description": "CodeExplanationDialog component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -74,7 +74,7 @@ "category": "layout", "canHaveChildren": true, "description": "ComponentBindingDialog component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -93,7 +93,7 @@ "category": "layout", "canHaveChildren": true, "description": "DataSourceCard component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -103,7 +103,7 @@ "category": "layout", "canHaveChildren": true, "description": "DataSourceEditorDialog component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -230,7 +230,7 @@ "category": "layout", "canHaveChildren": true, "description": "TreeCard component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -240,7 +240,7 @@ "category": "layout", "canHaveChildren": true, "description": "TreeFormDialog component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -547,7 +547,7 @@ "category": "input", "canHaveChildren": true, "description": "ToolbarButton component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -728,7 +728,7 @@ "category": "display", "canHaveChildren": true, "description": "SchemaCodeViewer organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -873,7 +873,7 @@ "category": "navigation", "canHaveChildren": true, "description": "FileTabs component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -920,7 +920,7 @@ "category": "navigation", "canHaveChildren": true, "description": "NavigationItem component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -930,7 +930,7 @@ "category": "navigation", "canHaveChildren": true, "description": "NavigationMenu component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1012,7 +1012,7 @@ "category": "feedback", "canHaveChildren": true, "description": "EmptyCanvasState organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1050,7 +1050,7 @@ "category": "feedback", "canHaveChildren": true, "description": "Empty state placeholder", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1154,7 +1154,7 @@ "category": "feedback", "canHaveChildren": true, "description": "SchemaEditorStatusBar organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1209,7 +1209,7 @@ "category": "data", "canHaveChildren": true, "description": "DataSourceManager organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1322,7 +1322,7 @@ "category": "data", "canHaveChildren": false, "description": "Statistic card display", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1359,7 +1359,7 @@ "category": "data", "canHaveChildren": true, "description": "TreeListHeader component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1369,7 +1369,7 @@ "category": "data", "canHaveChildren": true, "description": "TreeListPanel organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1397,7 +1397,7 @@ "category": "custom", "canHaveChildren": false, "description": "Action button toolbar", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1417,7 +1417,7 @@ "category": "custom", "canHaveChildren": true, "description": "AppHeader organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1445,7 +1445,7 @@ "category": "custom", "canHaveChildren": true, "description": "BindingEditor component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1482,7 +1482,7 @@ "category": "custom", "canHaveChildren": true, "description": "CanvasRenderer component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1546,7 +1546,7 @@ "category": "custom", "canHaveChildren": true, "description": "ComponentPalette component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1565,7 +1565,7 @@ "category": "custom", "canHaveChildren": true, "description": "ComponentTree component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1584,7 +1584,7 @@ "category": "custom", "canHaveChildren": false, "description": "Custom data display card", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1612,7 +1612,7 @@ "category": "custom", "canHaveChildren": true, "description": "EditorActions component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1622,7 +1622,7 @@ "category": "custom", "canHaveChildren": true, "description": "EditorToolbar component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1641,7 +1641,7 @@ "category": "custom", "canHaveChildren": true, "description": "JSONUIShowcase organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1660,7 +1660,7 @@ "category": "custom", "canHaveChildren": true, "description": "LazyInlineMonacoEditor component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1670,7 +1670,7 @@ "category": "custom", "canHaveChildren": true, "description": "LazyMonacoEditor component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1689,7 +1689,7 @@ "category": "custom", "canHaveChildren": true, "description": "MonacoEditorPanel component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1718,7 +1718,7 @@ "category": "custom", "canHaveChildren": true, "description": "PageHeaderContent component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1764,7 +1764,7 @@ "category": "custom", "canHaveChildren": true, "description": "PropertyEditor component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1820,7 +1820,7 @@ "category": "custom", "canHaveChildren": true, "description": "SchemaEditorCanvas organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1830,7 +1830,7 @@ "category": "custom", "canHaveChildren": true, "description": "SchemaEditorLayout organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1840,7 +1840,7 @@ "category": "custom", "canHaveChildren": true, "description": "SchemaEditorPropertiesPanel organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1850,7 +1850,7 @@ "category": "custom", "canHaveChildren": true, "description": "SchemaEditorSidebar organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1860,7 +1860,7 @@ "category": "custom", "canHaveChildren": true, "description": "SchemaEditorToolbar organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -1888,7 +1888,7 @@ "category": "custom", "canHaveChildren": true, "description": "SearchBar component", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1907,7 +1907,7 @@ "category": "custom", "canHaveChildren": false, "description": "Search input with icon", - "status": "maybe-json-compatible", + "status": "supported", "source": "molecules", "jsonCompatible": true }, @@ -1999,7 +1999,7 @@ "category": "custom", "canHaveChildren": true, "description": "ToolbarActions organism component", - "status": "maybe-json-compatible", + "status": "supported", "source": "organisms", "jsonCompatible": true }, @@ -2023,11 +2023,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/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' From 79732ce3582fde909c56fc4a6002f7d33918e557 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:39:46 +0000 Subject: [PATCH 13/18] docs: update JSON compatibility table --- JSON_COMPATIBILITY_IMPLEMENTATION.md | 38 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) 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. From ec78ec0f9beb553fd68ccf1bdf7bf07587cb28af Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:40:16 +0000 Subject: [PATCH 14/18] Add JSON UI schema linting --- package.json | 5 +- scripts/lint-json-ui-schemas.cjs | 252 +++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 scripts/lint-json-ui-schemas.cjs diff --git a/package.json b/package.json index 67726b9..a6d9f27 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "kill": "fuser -k 5000/tcp", "prebuild": "mkdir -p /tmp/dist || true", "build": "tsc -b --noCheck && vite build", - "lint": "eslint . --fix", - "lint:check": "eslint .", + "lint": "eslint . --fix && npm run lint:schemas", + "lint:check": "eslint . && npm run lint:schemas", + "lint:schemas": "node scripts/lint-json-ui-schemas.cjs", "optimize": "vite optimize", "preview": "vite preview --host 0.0.0.0 --port ${PORT:-80}", "test:e2e": "playwright test", 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.') From d7250456715e193851babab72bf353fc4bda85e8 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:40:51 +0000 Subject: [PATCH 15/18] Add schema validation for JSON UI --- package-lock.json | 58 ++++++ package.json | 4 +- scripts/validate-json-schemas.ts | 297 +++++++++++++++++++++++++++++++ 3 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-json-schemas.ts diff --git a/package-lock.json b/package-lock.json index 3fa92d6..fb82796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^17.0.0", "tailwindcss": "^4.1.11", + "tsx": "^4.21.0", "typescript": "~5.7.2", "typescript-eslint": "^8.38.0", "vite": "^7.3.1" @@ -5700,6 +5701,19 @@ "node": ">=6" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "dev": true, @@ -6842,6 +6856,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/robust-predicates": { "version": "3.0.2", "license": "Unlicense" @@ -7117,6 +7141,40 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "license": "MIT", diff --git a/package.json b/package.json index 67726b9..ff91f79 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "kill": "fuser -k 5000/tcp", "prebuild": "mkdir -p /tmp/dist || true", - "build": "tsc -b --noCheck && vite build", + "build": "npm run schemas:validate && tsc -b --noCheck && vite build", "lint": "eslint . --fix", "lint:check": "eslint .", "optimize": "vite optimize", @@ -21,6 +21,7 @@ "pages:list": "node scripts/list-pages.js", "pages:validate": "tsx src/config/validate-config.ts", "pages:generate": "node scripts/generate-page.js", + "schemas:validate": "tsx scripts/validate-json-schemas.ts", "components:list": "node scripts/list-json-components.cjs", "components:scan": "node scripts/scan-and-update-registry.cjs", "components:validate": "node scripts/validate-supported-components.cjs" @@ -107,6 +108,7 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^17.0.0", "tailwindcss": "^4.1.11", + "tsx": "^4.21.0", "typescript": "~5.7.2", "typescript-eslint": "^8.38.0", "vite": "^7.3.1" diff --git a/scripts/validate-json-schemas.ts b/scripts/validate-json-schemas.ts new file mode 100644 index 0000000..f6afb74 --- /dev/null +++ b/scripts/validate-json-schemas.ts @@ -0,0 +1,297 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { UIComponentSchema } from '../src/lib/json-ui/schema' + +interface ComponentDefinitionProp { + name: string + type: 'string' | 'number' | 'boolean' + options?: Array +} + +interface ComponentDefinition { + type: string + props?: ComponentDefinitionProp[] +} + +interface ComponentNode { + component: Record + path: string +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..') + +const componentDefinitionsPath = path.join(rootDir, 'src/lib/component-definitions.json') +const componentRegistryPath = path.join(rootDir, 'src/lib/json-ui/component-registry.ts') +const jsonRegistryPath = path.join(rootDir, 'json-components-registry.json') + +const readJson = (filePath: string) => JSON.parse(fs.readFileSync(filePath, 'utf8')) +const readText = (filePath: string) => fs.readFileSync(filePath, 'utf8') + +const componentDefinitions = readJson(componentDefinitionsPath) as ComponentDefinition[] +const componentDefinitionMap = new Map(componentDefinitions.map((def) => [def.type, def])) + +const jsonRegistry = readJson(jsonRegistryPath) as { + components?: Array<{ type?: string; name?: string; export?: string }> +} + +const extractObjectLiteral = (content: string, marker: string) => { + const markerIndex = content.indexOf(marker) + if (markerIndex === -1) { + throw new Error(`Unable to locate ${marker} in component registry file`) + } + const braceStart = content.indexOf('{', markerIndex) + if (braceStart === -1) { + throw new Error(`Unable to locate opening brace for ${marker}`) + } + let depth = 0 + for (let i = braceStart; i < content.length; i += 1) { + const char = content[i] + if (char === '{') depth += 1 + if (char === '}') depth -= 1 + if (depth === 0) { + return content.slice(braceStart, i + 1) + } + } + throw new Error(`Unable to locate closing brace for ${marker}`) +} + +const extractKeysFromObjectLiteral = (literal: string) => { + const body = literal.trim().replace(/^\{/, '').replace(/\}$/, '') + const entries = body + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + const keys = new Set() + + entries.forEach((entry) => { + if (entry.startsWith('...')) { + return + } + const [keyPart] = entry.split(':') + const key = keyPart.trim() + if (key) { + keys.add(key) + } + }) + + return keys +} + +const componentRegistryContent = readText(componentRegistryPath) +const primitiveKeys = extractKeysFromObjectLiteral( + extractObjectLiteral(componentRegistryContent, 'export const primitiveComponents') +) +const shadcnKeys = extractKeysFromObjectLiteral( + extractObjectLiteral(componentRegistryContent, 'export const shadcnComponents') +) +const wrapperKeys = extractKeysFromObjectLiteral( + extractObjectLiteral(componentRegistryContent, 'export const jsonWrapperComponents') +) +const iconKeys = extractKeysFromObjectLiteral( + extractObjectLiteral(componentRegistryContent, 'export const iconComponents') +) + +const registryTypes = new Set( + (jsonRegistry.components ?? []) + .map((entry) => entry.type ?? entry.name ?? entry.export) + .filter((value): value is string => Boolean(value)) +) + +const validComponentTypes = new Set([ + ...primitiveKeys, + ...shadcnKeys, + ...wrapperKeys, + ...iconKeys, + ...componentDefinitions.map((def) => def.type), + ...registryTypes, +]) + +const schemaRoots = [ + path.join(rootDir, 'src/config'), + path.join(rootDir, 'src/data'), +] + +const collectJsonFiles = (dir: string, files: string[] = []) => { + if (!fs.existsSync(dir)) { + return files + } + const entries = fs.readdirSync(dir, { withFileTypes: true }) + entries.forEach((entry) => { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + collectJsonFiles(fullPath, files) + return + } + if (entry.isFile() && entry.name.endsWith('.json')) { + files.push(fullPath) + } + }) + return files +} + +const isComponentNode = (value: unknown): value is Record => { + if (!value || typeof value !== 'object') { + return false + } + const candidate = value as Record + if (typeof candidate.id !== 'string' || typeof candidate.type !== 'string') { + return false + } + return ( + 'props' in candidate || + 'children' in candidate || + 'className' in candidate || + 'bindings' in candidate || + 'events' in candidate || + 'dataBinding' in candidate || + 'style' in candidate + ) +} + +const findComponents = (value: unknown, currentPath: string): ComponentNode[] => { + const components: ComponentNode[] = [] + if (Array.isArray(value)) { + value.forEach((item, index) => { + components.push(...findComponents(item, `${currentPath}[${index}]`)) + }) + return components + } + if (!value || typeof value !== 'object') { + return components + } + + const candidate = value as Record + if (isComponentNode(candidate)) { + components.push({ component: candidate, path: currentPath }) + } + + Object.entries(candidate).forEach(([key, child]) => { + const nextPath = currentPath ? `${currentPath}.${key}` : key + components.push(...findComponents(child, nextPath)) + }) + + return components +} + +const isTemplateBinding = (value: unknown) => + typeof value === 'string' && value.includes('{{') && value.includes('}}') + +const validateProps = ( + component: Record, + filePath: string, + componentPath: string, + errors: string[] +) => { + const definition = componentDefinitionMap.get(component.type as string) + const props = component.props + + if (!definition || !definition.props || !props || typeof props !== 'object') { + return + } + + const propDefinitions = new Map(definition.props.map((prop) => [prop.name, prop])) + + Object.entries(props as Record).forEach(([propName, propValue]) => { + const propDefinition = propDefinitions.get(propName) + if (!propDefinition) { + errors.push( + `${filePath} -> ${componentPath}: Unknown prop "${propName}" for component type "${component.type}"` + ) + return + } + + const expectedType = propDefinition.type + const actualType = Array.isArray(propValue) ? 'array' : typeof propValue + + if ( + expectedType === 'string' && + actualType !== 'string' && + propValue !== undefined + ) { + errors.push( + `${filePath} -> ${componentPath}: Prop "${propName}" expected string but got ${actualType}` + ) + return + } + + if ( + expectedType === 'number' && + actualType !== 'number' && + !isTemplateBinding(propValue) + ) { + errors.push( + `${filePath} -> ${componentPath}: Prop "${propName}" expected number but got ${actualType}` + ) + return + } + + if ( + expectedType === 'boolean' && + actualType !== 'boolean' && + !isTemplateBinding(propValue) + ) { + errors.push( + `${filePath} -> ${componentPath}: Prop "${propName}" expected boolean but got ${actualType}` + ) + return + } + + if (propDefinition.options && propValue !== undefined) { + if (!propDefinition.options.includes(propValue as string | number | boolean)) { + errors.push( + `${filePath} -> ${componentPath}: Prop "${propName}" value must be one of ${propDefinition.options.join(', ')}` + ) + } + } + }) +} + +const validateComponentsInFile = (filePath: string, errors: string[]) => { + let parsed: unknown + try { + parsed = readJson(filePath) + } catch (error) { + errors.push(`${filePath}: Unable to parse JSON - ${(error as Error).message}`) + return + } + + const components = findComponents(parsed, 'root') + if (components.length === 0) { + return + } + + components.forEach(({ component, path: componentPath }) => { + const parseResult = UIComponentSchema.safeParse(component) + if (!parseResult.success) { + const issueMessages = parseResult.error.issues + .map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`) + .join('\n') + errors.push( + `${filePath} -> ${componentPath}: Schema validation failed\n${issueMessages}` + ) + } + + if (!validComponentTypes.has(component.type as string)) { + errors.push( + `${filePath} -> ${componentPath}: Unknown component type "${component.type}"` + ) + } + + validateProps(component, filePath, componentPath, errors) + }) +} + +const jsonFiles = schemaRoots.flatMap((dir) => collectJsonFiles(dir)) +const errors: string[] = [] + +jsonFiles.forEach((filePath) => validateComponentsInFile(filePath, errors)) + +if (errors.length > 0) { + console.error('JSON schema validation failed:') + errors.forEach((error) => console.error(`- ${error}`)) + process.exit(1) +} + +console.log('JSON schema validation passed.') From 37442350cd7514da6173cd8d89afe3d0a4bec8b3 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:47:43 +0000 Subject: [PATCH 16/18] Add dev QA smoke fixture schemas --- fixtures/dev-qa/README.md | 13 +++++++ fixtures/dev-qa/custom.json | 40 ++++++++++++++++++++ fixtures/dev-qa/data.json | 46 ++++++++++++++++++++++ fixtures/dev-qa/display.json | 42 +++++++++++++++++++++ fixtures/dev-qa/feedback.json | 40 ++++++++++++++++++++ fixtures/dev-qa/input.json | 45 ++++++++++++++++++++++ fixtures/dev-qa/layout.json | 67 +++++++++++++++++++++++++++++++++ fixtures/dev-qa/navigation.json | 42 +++++++++++++++++++++ 8 files changed, 335 insertions(+) create mode 100644 fixtures/dev-qa/README.md create mode 100644 fixtures/dev-qa/custom.json create mode 100644 fixtures/dev-qa/data.json create mode 100644 fixtures/dev-qa/display.json create mode 100644 fixtures/dev-qa/feedback.json create mode 100644 fixtures/dev-qa/input.json create mode 100644 fixtures/dev-qa/layout.json create mode 100644 fixtures/dev-qa/navigation.json diff --git a/fixtures/dev-qa/README.md b/fixtures/dev-qa/README.md new file mode 100644 index 0000000..b2d785c --- /dev/null +++ b/fixtures/dev-qa/README.md @@ -0,0 +1,13 @@ +# Dev/QA Smoke Fixture Schemas + +These JSON schemas provide lightweight smoke-test coverage for each JSON UI component category. +Each file is a standalone page schema that can be loaded in dev or QA to verify rendering. + +## Categories +- `layout.json` +- `input.json` +- `display.json` +- `navigation.json` +- `feedback.json` +- `data.json` +- `custom.json` diff --git a/fixtures/dev-qa/custom.json b/fixtures/dev-qa/custom.json new file mode 100644 index 0000000..df784a9 --- /dev/null +++ b/fixtures/dev-qa/custom.json @@ -0,0 +1,40 @@ +{ + "id": "smoke-custom", + "name": "Smoke Custom", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "custom-section", + "type": "section", + "children": [ + { + "id": "custom-heading", + "type": "Heading", + "props": { + "level": 3, + "children": "Custom Component Smoke Check" + } + }, + { + "id": "custom-data-card", + "type": "DataCard", + "props": { + "title": "QA Metric", + "value": "99%", + "icon": "TrendUp" + } + }, + { + "id": "custom-search", + "type": "SearchInput", + "props": { + "placeholder": "Search QA fixtures..." + } + } + ] + } + ] +} diff --git a/fixtures/dev-qa/data.json b/fixtures/dev-qa/data.json new file mode 100644 index 0000000..cf83a1c --- /dev/null +++ b/fixtures/dev-qa/data.json @@ -0,0 +1,46 @@ +{ + "id": "smoke-data", + "name": "Smoke Data", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "data-section", + "type": "section", + "children": [ + { + "id": "data-heading", + "type": "Heading", + "props": { + "level": 3, + "children": "Data Smoke Check" + } + }, + { + "id": "data-list", + "type": "List", + "props": { + "items": ["QA record A", "QA record B", "QA record C"], + "emptyMessage": "No QA records" + } + }, + { + "id": "data-table", + "type": "Table", + "props": { + "columns": [ + { "key": "name", "header": "Name" }, + { "key": "status", "header": "Status" } + ], + "data": [ + { "name": "Smoke Run", "status": "Pass" }, + { "name": "Regression", "status": "Pending" } + ] + } + } + ] + } + ] +} diff --git a/fixtures/dev-qa/display.json b/fixtures/dev-qa/display.json new file mode 100644 index 0000000..736940e --- /dev/null +++ b/fixtures/dev-qa/display.json @@ -0,0 +1,42 @@ +{ + "id": "smoke-display", + "name": "Smoke Display", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "display-section", + "type": "section", + "children": [ + { + "id": "display-heading", + "type": "Heading", + "props": { + "level": 3, + "children": "Display Smoke Check" + } + }, + { + "id": "display-text", + "type": "Text", + "props": { + "children": "Checks text, badges, and separators for QA verification." + } + }, + { + "id": "display-badge", + "type": "Badge", + "props": { + "children": "QA" + } + }, + { + "id": "display-divider", + "type": "Divider" + } + ] + } + ] +} diff --git a/fixtures/dev-qa/feedback.json b/fixtures/dev-qa/feedback.json new file mode 100644 index 0000000..add7d71 --- /dev/null +++ b/fixtures/dev-qa/feedback.json @@ -0,0 +1,40 @@ +{ + "id": "smoke-feedback", + "name": "Smoke Feedback", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "feedback-section", + "type": "section", + "children": [ + { + "id": "feedback-heading", + "type": "Heading", + "props": { + "level": 3, + "children": "Feedback Smoke Check" + } + }, + { + "id": "feedback-alert", + "type": "Alert", + "props": { + "variant": "info", + "children": "QA info alert rendered." + } + }, + { + "id": "feedback-status", + "type": "StatusBadge", + "props": { + "status": "active", + "children": "Active" + } + } + ] + } + ] +} diff --git a/fixtures/dev-qa/input.json b/fixtures/dev-qa/input.json new file mode 100644 index 0000000..f592b79 --- /dev/null +++ b/fixtures/dev-qa/input.json @@ -0,0 +1,45 @@ +{ + "id": "smoke-input", + "name": "Smoke Input", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "input-section", + "type": "section", + "children": [ + { + "id": "input-heading", + "type": "Heading", + "props": { + "level": 2, + "children": "Input Smoke Check" + } + }, + { + "id": "input-control", + "type": "Input", + "props": { + "placeholder": "Enter QA value..." + } + }, + { + "id": "input-toggle", + "type": "Switch", + "props": { + "checked": true + } + }, + { + "id": "input-button", + "type": "Button", + "props": { + "children": "Submit" + } + } + ] + } + ] +} diff --git a/fixtures/dev-qa/layout.json b/fixtures/dev-qa/layout.json new file mode 100644 index 0000000..33565e3 --- /dev/null +++ b/fixtures/dev-qa/layout.json @@ -0,0 +1,67 @@ +{ + "id": "smoke-layout", + "name": "Smoke Layout", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "layout-container", + "type": "Container", + "props": { + "className": "py-6" + }, + "children": [ + { + "id": "layout-stack", + "type": "Stack", + "props": { + "gap": 4 + }, + "children": [ + { + "id": "layout-card", + "type": "Card", + "children": [ + { + "id": "layout-card-header", + "type": "CardHeader", + "children": [ + { + "id": "layout-card-title", + "type": "CardTitle", + "props": { + "children": "Layout Smoke Check" + } + }, + { + "id": "layout-card-description", + "type": "CardDescription", + "props": { + "children": "Ensures layout primitives render in QA." + } + } + ] + }, + { + "id": "layout-card-content", + "type": "CardContent", + "children": [ + { + "id": "layout-card-text", + "type": "Text", + "props": { + "children": "This card is wrapped in Container and Stack components." + } + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/fixtures/dev-qa/navigation.json b/fixtures/dev-qa/navigation.json new file mode 100644 index 0000000..9095c36 --- /dev/null +++ b/fixtures/dev-qa/navigation.json @@ -0,0 +1,42 @@ +{ + "id": "smoke-navigation", + "name": "Smoke Navigation", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "navigation-section", + "type": "section", + "children": [ + { + "id": "navigation-heading", + "type": "Heading", + "props": { + "level": 3, + "children": "Navigation Smoke Check" + } + }, + { + "id": "navigation-link", + "type": "Link", + "props": { + "href": "/qa", + "children": "Go to QA overview" + } + }, + { + "id": "navigation-breadcrumb", + "type": "Breadcrumb", + "props": { + "items": [ + { "label": "Home", "href": "/" }, + { "label": "QA" } + ] + } + } + ] + } + ] +} From 571fe3ef2cbd8feab87cda7e888786b506e9c6d9 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 12:49:03 +0000 Subject: [PATCH 17/18] Add deprecation guidance and schema warnings --- JSON_COMPONENTS.md | 23 +++++++++++++++++++++++ json-components-registry.json | 8 ++++++-- src/lib/json-ui/component-registry.ts | 24 ++++++++++++++++++++++++ src/lib/schema-renderer.tsx | 23 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/JSON_COMPONENTS.md b/JSON_COMPONENTS.md index 03ae1d0..54311db 100644 --- a/JSON_COMPONENTS.md +++ b/JSON_COMPONENTS.md @@ -229,6 +229,29 @@ Priority for migration: 3. Components with good atomic design 4. Components without complex state management +## Deprecation & JSON Migration Process + +Use this process when retiring legacy components or renaming JSON component types. + +### Deprecation workflow +1. **Assess usage**: Identify schemas and component usage (search in `src/config/schemas` or project JSON). Flag any external consumers. +2. **Define a replacement**: Ensure a supported JSON-safe replacement exists (or create a wrapper) and document prop differences. +3. **Mark deprecated in the registry**: Update `json-components-registry.json` with `"status": "deprecated"` and optional `"deprecated"` metadata: + - `replacedBy`: the new component type to use. + - `message`: extra guidance for migrations. +4. **Update definitions**: If needed, adjust `src/lib/component-definitions.ts` to align defaults and prop expectations for the replacement. +5. **Communicate the change**: Add a note to release documentation or changelog and note the replacement. +6. **Set a removal window**: Target the next minor/major release for removal once migration is complete. + +### JSON migration checklist +- [ ] Replace deprecated component types in schemas with their replacements. +- [ ] Validate props against the replacement component definition. +- [ ] Run JSON renderer previews to confirm layout and bindings. +- [ ] Remove any legacy bindings or props that are no longer supported. + +### Runtime warning mechanism +The schema renderer emits a `console.warn` when a deprecated component type appears in a JSON schema. This warning uses the metadata in `json-components-registry.json` to suggest replacements and highlight remediation guidance. + ## Related Documentation - [PRD.md](./PRD.md) - Product requirements document diff --git a/json-components-registry.json b/json-components-registry.json index 94efde8..06c73dd 100644 --- a/json-components-registry.json +++ b/json-components-registry.json @@ -421,8 +421,12 @@ "category": "input", "canHaveChildren": false, "description": "Multi-line text input", - "status": "supported", - "source": "atoms" + "status": "deprecated", + "source": "atoms", + "deprecated": { + "replacedBy": "Textarea", + "message": "Prefer the shadcn/ui Textarea component for JSON schemas." + } }, { "type": "Toggle", diff --git a/src/lib/json-ui/component-registry.ts b/src/lib/json-ui/component-registry.ts index 2912d38..ba445ac 100644 --- a/src/lib/json-ui/component-registry.ts +++ b/src/lib/json-ui/component-registry.ts @@ -47,12 +47,19 @@ interface JsonRegistryEntry { type?: string export?: string source?: string + status?: string + deprecated?: DeprecatedComponentInfo } interface JsonComponentRegistry { components?: JsonRegistryEntry[] } +export interface DeprecatedComponentInfo { + replacedBy?: string + message?: string +} + const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry const buildRegistryFromNames = ( @@ -69,6 +76,19 @@ const buildRegistryFromNames = ( } const jsonRegistryEntries = jsonRegistry.components ?? [] +const deprecatedComponentInfo = jsonRegistryEntries.reduce>( + (acc, entry) => { + const entryName = entry.export ?? entry.name ?? entry.type + if (!entryName) { + return acc + } + if (entry.status === 'deprecated' || entry.deprecated) { + acc[entryName] = entry.deprecated ?? {} + } + return acc + }, + {} +) const atomRegistryNames = jsonRegistryEntries .filter((entry) => entry.source === 'atoms') .map((entry) => entry.export ?? entry.name ?? entry.type) @@ -237,3 +257,7 @@ export function getUIComponent(type: string): ComponentType | string | null export function hasComponent(type: string): boolean { return type in uiComponentRegistry } + +export function getDeprecatedComponentInfo(type: string): DeprecatedComponentInfo | null { + return deprecatedComponentInfo[type] ?? null +} diff --git a/src/lib/schema-renderer.tsx b/src/lib/schema-renderer.tsx index 136612c..02ed0fc 100644 --- a/src/lib/schema-renderer.tsx +++ b/src/lib/schema-renderer.tsx @@ -2,6 +2,7 @@ import { createElement, type ComponentType, type ReactNode } from 'react' import { cn } from '@/lib/utils' import { Component as ComponentSchema, Layout } from '@/schemas/ui-schema' import { useDataBinding, useEventHandlers, useComponentRegistry } from '@/hooks/ui' +import { getDeprecatedComponentInfo } from '@/lib/json-ui/component-registry' interface SchemaRendererProps { schema: ComponentSchema @@ -15,6 +16,26 @@ interface LayoutRendererProps { children: ReactNode } +const warnedDeprecatedComponents = new Set() + +const warnDeprecatedComponent = (schema: ComponentSchema) => { + const deprecatedInfo = getDeprecatedComponentInfo(schema.type) + if (!deprecatedInfo || warnedDeprecatedComponents.has(schema.type)) { + return + } + + const idSuffix = schema.id ? ` (id: ${schema.id})` : '' + const replacementHint = deprecatedInfo.replacedBy + ? ` Replace with "${deprecatedInfo.replacedBy}".` + : '' + const extraMessage = deprecatedInfo.message ? ` ${deprecatedInfo.message}` : '' + + console.warn( + `[SchemaRenderer] Deprecated component "${schema.type}" detected in schema${idSuffix}.${replacementHint}${extraMessage}` + ) + warnedDeprecatedComponents.add(schema.type) +} + function LayoutRenderer({ layout, children }: LayoutRendererProps) { const getLayoutClasses = () => { const classes: string[] = [] @@ -85,6 +106,8 @@ export function SchemaRenderer({ schema, data, functions = {}, componentRegistry ) } + warnDeprecatedComponent(schema) + const props = resolveProps(schema.props || {}) const events = resolveEvents(schema.events) const combinedProps = { ...props, ...events } From 0a491528f3006c4fe28e7a836ddfecd99eb70bae Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 13:03:37 +0000 Subject: [PATCH 18/18] Add progress components to JSON UI schemas --- src/lib/component-definitions.json | 12 +++-- src/lib/json-ui/component-registry.ts | 4 ++ src/schemas/page-schemas.json | 76 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/lib/component-definitions.json b/src/lib/component-definitions.json index 9731f20..784eb4d 100644 --- a/src/lib/component-definitions.json +++ b/src/lib/component-definitions.json @@ -185,14 +185,20 @@ "label": "Progress Bar", "category": "display", "icon": "ChartBar", - "defaultProps": { "value": 65, "size": "md", "variant": "default", "showLabel": false } + "defaultProps": { + "value": 65, + "max": 100, + "size": "md", + "variant": "default", + "showLabel": false + } }, { "type": "CircularProgress", "label": "Circular Progress", "category": "display", "icon": "CircleNotch", - "defaultProps": { "value": 65, "size": "md", "showLabel": true } + "defaultProps": { "value": 65, "max": 100, "size": "md", "showLabel": true } }, { "type": "Spinner", @@ -220,7 +226,7 @@ "label": "Divider", "category": "display", "icon": "Minus", - "defaultProps": { "orientation": "horizontal", "decorative": true } + "defaultProps": { "orientation": "horizontal", "decorative": true, "className": "" } }, { "type": "Link", diff --git a/src/lib/json-ui/component-registry.ts b/src/lib/json-ui/component-registry.ts index 90df225..6d9315f 100644 --- a/src/lib/json-ui/component-registry.ts +++ b/src/lib/json-ui/component-registry.ts @@ -17,6 +17,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { CircularProgress, Divider, ProgressBar } from '@/components/atoms' import * as AtomComponents from '@/components/atoms' import * as MoleculeComponents from '@/components/molecules' import * as OrganismComponents from '@/components/organisms' @@ -175,6 +176,9 @@ export const atomComponents: UIComponentRegistry = { atomRegistryNames, AtomComponents as Record> ), + CircularProgress, + Divider, + ProgressBar, DataList: (AtomComponents as Record>).DataList, DataTable: (AtomComponents as Record>).DataTable, MetricCard: (AtomComponents as Record>).MetricCard, diff --git a/src/schemas/page-schemas.json b/src/schemas/page-schemas.json index 0a38f9a..8521334 100644 --- a/src/schemas/page-schemas.json +++ b/src/schemas/page-schemas.json @@ -208,5 +208,81 @@ ] } ] + }, + "progressComponentsDemoSchema": { + "id": "progress-components-demo", + "name": "Progress Components Demo", + "layout": { + "type": "single" + }, + "dataSources": [ + { + "id": "progressValues", + "type": "static", + "defaultValue": { + "linear": 72, + "circular": 48, + "max": 100 + } + } + ], + "components": [ + { + "id": "progress-components-root", + "type": "div", + "props": { + "className": "space-y-6 rounded-lg border border-border bg-card p-6" + }, + "children": [ + { + "id": "progress-components-title", + "type": "Heading", + "props": { + "className": "text-xl font-semibold", + "children": "Progress Indicators" + } + }, + { + "id": "progress-components-divider-top", + "type": "Divider", + "props": { + "orientation": "horizontal" + } + }, + { + "id": "progress-components-linear", + "type": "ProgressBar", + "props": { + "size": "md", + "variant": "accent", + "showLabel": true + }, + "bindings": { + "value": { "source": "progressValues", "sourceType": "data", "path": "linear" }, + "max": { "source": "progressValues", "sourceType": "data", "path": "max" } + } + }, + { + "id": "progress-components-divider-middle", + "type": "Divider", + "props": { + "orientation": "horizontal" + } + }, + { + "id": "progress-components-circular", + "type": "CircularProgress", + "props": { + "size": "md", + "showLabel": true + }, + "bindings": { + "value": { "source": "progressValues", "sourceType": "data", "path": "circular" }, + "max": { "source": "progressValues", "sourceType": "data", "path": "max" } + } + } + ] + } + ] } }