Compare commits

..
Author SHA1 Message Date
Chris Tate 0bc3ae75d3 update docs 2026-02-09 01:41:06 -06:00
27 changed files with 96 additions and 345 deletions
+9
View File
@@ -0,0 +1,9 @@
---
"@json-render/core": minor
"@json-render/react": minor
"@json-render/react-native": minor
"@json-render/remotion": minor
"@json-render/codegen": minor
---
Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
+15 -16
View File
@@ -1,8 +1,8 @@
# json-render
**The framework for User-Generated Interfaces (UGI).**
**Predictable. Guardrailed. Fast.**
Dynamic, personalized UIs per user without sacrificing reliability. Predefined components and actions for safe, predictable output.
Let end users generate dashboards, widgets, apps, and videos from prompts — safely constrained to components you define.
```bash
npm install @json-render/core @json-render/react
@@ -14,12 +14,11 @@ npm install @json-render/core @json-render/remotion
## Why json-render?
json-render enables **User-Generated Interfaces**: dynamic UIs that end users create through natural language prompts, powered by Generative UI. You define the guardrails, AI generates within them:
When users prompt for UI, you need guarantees. json-render gives AI a **constrained vocabulary** so output is always predictable:
- **Guardrailed** - AI can only use components in your catalog
- **Predictable** - JSON output matches your schema, every time
- **Fast** - Stream and render progressively as the model responds
- **Cross-Platform** - React (web) and React Native (mobile) from the same catalog
- **Guardrailed** — AI can only use components in your catalog
- **Predictable** — JSON output matches your schema, every time
- **Fast** — Stream and render progressively as the model responds
## Quick Start
@@ -238,8 +237,8 @@ Any prop value can be data-driven using expressions:
Two expression forms:
- **`{ "$path": "/state/key" }`** - reads a value from the data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a condition (same syntax as visibility conditions) and picks a branch
- **`{ "$path": "/state/key" }`** -- reads a value from the data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- evaluates a condition (same syntax as visibility conditions) and picks a branch
### Actions
@@ -266,9 +265,9 @@ pnpm install
pnpm dev
```
- http://localhost:3000 - Docs & Playground
- http://localhost:3001 - Example Dashboard
- http://localhost:3002 - Remotion Video Example
- http://localhost:3000 -- Docs & Playground
- http://localhost:3001 -- Example Dashboard
- http://localhost:3002 -- Remotion Video Example
- React Native example: run `npx expo start` in `examples/react-native`
## How It Works
@@ -284,10 +283,10 @@ flowchart LR
D -.- G([streamed])
```
1. **Define the guardrails** - what components, actions, and data bindings AI can use
2. **Users generate** - end users describe what they want in natural language
3. **AI generates JSON** - output is always predictable, constrained to your catalog
4. **Render fast** - stream and render progressively as the model responds
1. **Define the guardrails** — what components, actions, and data bindings AI can use
2. **Users prompt** — end users describe what they want in natural language
3. **AI generates JSON** — output is always predictable, constrained to your catalog
4. **Render fast** — stream and render progressively as the model responds
## License
@@ -93,7 +93,7 @@ React Native renderer with standard components, providers, and hooks.
## defineRegistry
Create a type-safe component registry. Standard components are built-in; only register custom components.
Create a type-safe component registry. Standard components are built-in -- only register custom components.
```tsx
import { defineRegistry, type Components } from '@json-render/react-native';
+1 -1
View File
@@ -100,7 +100,7 @@ const fixed = autoFixSpec(spec);
### Improved: State Management
`DataProvider` has been renamed to `StateProvider` with a clearer API. State is now a first-class part of specs. Elements can bind to state via `$path` expressions, and the built-in `setState` action updates state directly.
`DataProvider` has been renamed to `StateProvider` with a clearer API. State is now a first-class part of specs -- elements can bind to state via `$path` expressions, and the built-in `setState` action updates state directly.
### Improved: AI Prompts
+7 -13
View File
@@ -2,19 +2,17 @@ export const metadata = { title: "Introduction" }
# Introduction
The framework for User-Generated Interfaces (UGI). Dynamic, personalized UIs per user without sacrificing reliability.
Predictable. Guardrailed. Fast. Let users generate dashboards, widgets, apps, and data visualizations from prompts.
## What is json-render?
json-render is the framework for **User-Generated Interfaces**: dynamic UIs that end users generate through natural language prompts, powered by Generative UI. You define the guardrails: what components exist, what props they take, what actions are available. AI generates JSON that matches your schema, and your components render it natively on web or mobile.
Every interface is unique to the user, but every interface is safe and predictable.
json-render lets end users generate UI from natural language prompts — safely constrained to components you define. You set the guardrails: what components exist, what props they take, what actions are available. AI generates JSON that matches your schema, and your components render it natively.
## Why json-render?
### Guardrailed
AI can only use components in your catalog. No arbitrary code generation. Predefined components and actions for safe, predictable output.
AI can only use components in your catalog. No arbitrary code generation.
### Predictable
@@ -24,13 +22,9 @@ JSON output matches your schema, every time. Actions are declared by name, you c
Stream and render progressively as the model responds. No waiting for completion.
### Cross-Platform
Render on web with React and on mobile with React Native from the same catalog and spec format.
## How it works
1. Define the guardrails - what components, actions, and data bindings AI can use
2. Users generate - end users describe what they want in natural language
3. AI generates JSON - output is always predictable, constrained to your catalog
4. Render fast - stream and render progressively as the model responds
1. Define the guardrails — what components, actions, and data bindings AI can use
2. Users prompt — end users describe what they want in natural language
3. AI generates JSON — output is always predictable, constrained to your catalog
4. Render fast — stream and render progressively as the model responds
+14 -17
View File
@@ -9,15 +9,12 @@ export default function Home() {
<>
{/* Hero */}
<section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
<p className="text-xs sm:text-sm font-medium text-muted-foreground tracking-widest uppercase mb-4">
The framework for User-Generated Interfaces
</p>
<h1 className="text-4xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
<h1 className="text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
AI → json-render → UI
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
Dynamic, personalized UIs per user without sacrificing reliability.
Predefined components and actions for safe, predictable output.
Define a component catalog. Users prompt. AI outputs JSON constrained
to your catalog. Your components render it.
</p>
<Demo />
@@ -68,10 +65,10 @@ export default function Home() {
<div className="text-xs text-muted-foreground font-mono mb-3">
02
</div>
<h3 className="text-lg font-semibold mb-2">Users Generate</h3>
<h3 className="text-lg font-semibold mb-2">Users Prompt</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
End users describe what they want. AI generates JSON constrained
to your catalog. Every interface is unique to the user.
to your catalog.
</p>
</div>
<div>
@@ -247,10 +244,6 @@ export default function Page() {
<h2 className="text-2xl font-semibold mb-12 text-center">Features</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
title: "User-Generated Interfaces",
desc: "Dynamic, personalized UIs per user powered by Generative UI",
},
{
title: "Guardrails",
desc: "AI can only use components you define in the catalog",
@@ -260,16 +253,20 @@ export default function Page() {
desc: "Progressive rendering as JSON streams from the model",
},
{
title: "React & React Native",
desc: "Render on web and mobile from the same catalog and spec format",
title: "Code Export",
desc: "Export as standalone React code with no runtime dependencies",
},
{
title: "Data Binding",
desc: "Two-way state binding with dynamic prop expressions",
desc: "Two-way binding with JSON Pointer paths",
},
{
title: "Code Export",
desc: "Export as standalone React code with no runtime dependencies",
title: "Actions",
desc: "Named actions handled by your application",
},
{
title: "Visibility",
desc: "Conditional show/hide based on data or auth",
},
].map((feature) => (
<div key={feature.title}>
+6 -10
View File
@@ -22,15 +22,11 @@ export const metadata: Metadata = {
template: "%s | json-render",
},
description:
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
keywords: [
"json-render",
"UGI",
"User-Generated Interfaces",
"AI UI generation",
"generative UI",
"React components",
"React Native",
"guardrails",
"structured output",
"dashboard builder",
@@ -42,23 +38,23 @@ export const metadata: Metadata = {
locale: "en_US",
url: "https://json-render.dev",
siteName: "json-render",
title: "json-render | The framework for User-Generated Interfaces",
title: "json-render | AI-generated UI with guardrails",
description:
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
images: [
{
url: "/og",
width: 1200,
height: 630,
alt: "json-render - The framework for User-Generated Interfaces",
alt: "json-render - AI-generated UI with guardrails",
},
],
},
twitter: {
card: "summary_large_image",
title: "json-render | The framework for User-Generated Interfaces",
title: "json-render | AI-generated UI with guardrails",
description:
"The framework for User-Generated Interfaces (UGI). Let users generate dashboards, widgets, and apps from prompts — safely constrained to components you define.",
"Let users generate dashboards, widgets, apps, and data visualizations from prompts — safely constrained to components you define.",
images: ["/og"],
creator: "@verabornnot",
},
+13 -17
View File
@@ -66,27 +66,23 @@ export async function renderOgImage(title: string) {
style={{
display: "flex",
flex: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{title.split("\n").map((line, i) => (
<span
key={i}
style={{
fontSize: 72,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
textAlign: "center",
lineHeight: 1.2,
}}
>
{line}
</span>
))}
<span
style={{
fontSize: 72,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
textAlign: "center",
lineHeight: 1.2,
}}
>
{title}
</span>
</div>
</div>,
{
+1 -1
View File
@@ -7,7 +7,7 @@
*/
export const PAGE_TITLES: Record<string, string> = {
// Home (no slug)
"": "The framework for\nUser-Generated Interfaces",
"": "AI-generated UI with guardrails",
// Top-level
playground: "Playground",
+8 -25
View File
@@ -823,12 +823,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const [localValue, setLocalValue] = useState<string>("");
const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue;
const rawOptions = props.options ?? [];
// Coerce options to strings – AI may produce objects/numbers instead of
// plain strings which would cause duplicate `[object Object]` keys.
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
const options = props.options ?? [];
return (
<div className="space-y-2">
@@ -844,11 +839,8 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
<SelectValue placeholder={props.placeholder ?? "Select..."} />
</SelectTrigger>
<SelectContent>
{options.map((opt, idx) => (
<SelectItem
key={`${idx}-${opt}`}
value={opt || `option-${idx}`}
>
{options.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
@@ -884,10 +876,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
},
Radio: ({ props, emit }) => {
const rawOptions = props.options ?? [];
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
const options = props.options ?? [];
const [boundValue, setBoundValue] = props.statePath
? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined];
@@ -905,17 +894,11 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
emit?.("change");
}}
>
{options.map((opt, idx) => (
<div
key={`${idx}-${opt}`}
className="flex items-center space-x-2"
>
<RadioGroupItem
value={opt || `option-${idx}`}
id={`${props.name}-${idx}-${opt}`}
/>
{options.map((opt) => (
<div key={opt} className="flex items-center space-x-2">
<RadioGroupItem value={opt} id={`${props.name}-${opt}`} />
<Label
htmlFor={`${props.name}-${idx}-${opt}`}
htmlFor={`${props.name}-${opt}`}
className="cursor-pointer"
>
{opt}
-17
View File
@@ -1,22 +1,5 @@
# @json-render/codegen
## 0.5.1
### Patch Changes
- @json-render/core@0.5.1
## 0.5.0
### Minor Changes
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
### Patch Changes
- Updated dependencies [3d2d1ad]
- @json-render/core@0.5.0
## 0.4.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/codegen",
"version": "0.5.1",
"version": "0.4.4",
"license": "Apache-2.0",
"description": "Utilities for generating code from json-render UI trees",
"keywords": [
-8
View File
@@ -1,13 +1,5 @@
# @json-render/core
## 0.5.1
## 0.5.0
### Minor Changes
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
## 0.4.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/core",
"version": "0.5.1",
"version": "0.4.4",
"license": "Apache-2.0",
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
"keywords": [
-19
View File
@@ -1,19 +0,0 @@
# @json-render/react-native
## 0.5.1
### Patch Changes
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
- @json-render/core@0.5.1
## 0.5.0
### Minor Changes
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
### Patch Changes
- Updated dependencies [3d2d1ad]
- @json-render/core@0.5.0
+1 -1
View File
@@ -59,7 +59,7 @@ export const { registry } = defineRegistry(catalog, {
});
```
Standard components (Container, Row, Column, Button, TextInput, etc.) are included by default. You only need to register custom ones.
Standard components (Container, Row, Column, Button, TextInput, etc.) are included by default -- you only need to register custom ones.
### 3. Render Specs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-native",
"version": "0.5.1",
"version": "0.4.3",
"license": "Apache-2.0",
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
"keywords": [
+3 -51
View File
@@ -1,6 +1,5 @@
import React, {
type ComponentType,
type ErrorInfo,
type ReactNode,
useCallback,
useMemo,
@@ -82,51 +81,6 @@ export interface RendererProps {
fallback?: ComponentRenderer;
}
// ---------------------------------------------------------------------------
// ElementErrorBoundary – catches rendering errors in individual elements so
// a single bad component never crashes the whole page.
// ---------------------------------------------------------------------------
interface ElementErrorBoundaryProps {
elementType: string;
children: ReactNode;
}
interface ElementErrorBoundaryState {
hasError: boolean;
}
class ElementErrorBoundary extends React.Component<
ElementErrorBoundaryProps,
ElementErrorBoundaryState
> {
constructor(props: ElementErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ElementErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(
`[json-render] Rendering error in <${this.props.elementType}>:`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
// Render nothing – the element silently disappears rather than
// crashing the entire application.
return null;
}
return this.props.children;
}
}
/**
* Element renderer component
*/
@@ -260,11 +214,9 @@ function ElementRenderer({
);
return (
<ElementErrorBoundary elementType={resolvedElement.type}>
<Component element={resolvedElement} emit={emit} loading={loading}>
{children}
</Component>
</ElementErrorBoundary>
<Component element={resolvedElement} emit={emit} loading={loading}>
{children}
</Component>
);
}
-18
View File
@@ -1,23 +1,5 @@
# @json-render/react
## 0.5.1
### Patch Changes
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
- @json-render/core@0.5.1
## 0.5.0
### Minor Changes
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
### Patch Changes
- Updated dependencies [3d2d1ad]
- @json-render/core@0.5.0
## 0.4.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react",
"version": "0.5.1",
"version": "0.4.4",
"license": "Apache-2.0",
"description": "React renderer for @json-render/core. JSON becomes React components.",
"keywords": [
+3 -51
View File
@@ -2,7 +2,6 @@
import React, {
type ComponentType,
type ErrorInfo,
type ReactNode,
useCallback,
useMemo,
@@ -78,51 +77,6 @@ export interface RendererProps {
fallback?: ComponentRenderer;
}
// ---------------------------------------------------------------------------
// ElementErrorBoundary – catches rendering errors in individual elements so
// a single bad component never crashes the whole page.
// ---------------------------------------------------------------------------
interface ElementErrorBoundaryProps {
elementType: string;
children: ReactNode;
}
interface ElementErrorBoundaryState {
hasError: boolean;
}
class ElementErrorBoundary extends React.Component<
ElementErrorBoundaryProps,
ElementErrorBoundaryState
> {
constructor(props: ElementErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ElementErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(
`[json-render] Rendering error in <${this.props.elementType}>:`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
// Render nothing – the element silently disappears rather than
// crashing the entire application.
return null;
}
return this.props.children;
}
}
/**
* Element renderer component
*/
@@ -256,11 +210,9 @@ function ElementRenderer({
);
return (
<ElementErrorBoundary elementType={resolvedElement.type}>
<Component element={resolvedElement} emit={emit} loading={loading}>
{children}
</Component>
</ElementErrorBoundary>
<Component element={resolvedElement} emit={emit} loading={loading}>
{children}
</Component>
);
}
-18
View File
@@ -1,23 +1,5 @@
# @json-render/remotion
## 0.5.1
### Patch Changes
- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.
- @json-render/core@0.5.1
## 0.5.0
### Minor Changes
- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.
### Patch Changes
- Updated dependencies [3d2d1ad]
- @json-render/core@0.5.0
## 0.4.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/remotion",
"version": "0.5.1",
"version": "0.4.4",
"license": "Apache-2.0",
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
"keywords": [
+1 -48
View File
@@ -1,6 +1,5 @@
"use client";
import React, { type ErrorInfo, type ReactNode } from "react";
import { AbsoluteFill, Sequence } from "remotion";
import type { TimelineSpec, ComponentRegistry, Clip } from "./types";
@@ -34,50 +33,6 @@ export const standardComponents: ComponentRegistry = {
VideoClip,
};
// ---------------------------------------------------------------------------
// ClipErrorBoundary – catches rendering errors in individual clips so
// a single bad clip never crashes the entire composition.
// ---------------------------------------------------------------------------
interface ClipErrorBoundaryProps {
clipId: string;
component: string;
children: ReactNode;
}
interface ClipErrorBoundaryState {
hasError: boolean;
}
class ClipErrorBoundary extends React.Component<
ClipErrorBoundaryProps,
ClipErrorBoundaryState
> {
constructor(props: ClipErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ClipErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error(
`[json-render/remotion] Rendering error in clip "${this.props.clipId}" (<${this.props.component}>):`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
interface RendererProps {
/** The timeline spec to render */
spec: TimelineSpec;
@@ -147,9 +102,7 @@ export function Renderer({
from={clip.from}
durationInFrames={clip.durationInFrames}
>
<ClipErrorBoundary clipId={clip.id} component={clip.component}>
<Component clip={clip} />
</ClipErrorBoundary>
<Component clip={clip} />
</Sequence>
);
};
+2 -2
View File
@@ -81,8 +81,8 @@ const finalSpec = compiler.getResult();
Any prop value can be a dynamic expression resolved at render time:
- **`{ "$path": "/state/key" }`** - reads a value from the state model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a visibility condition and picks a branch
- **`{ "$path": "/state/key" }`** -- reads a value from the state model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- evaluates a visibility condition and picks a branch
`$cond` uses the same syntax as visibility conditions (`eq`, `neq`, `path`, `and`, `or`, `not`). `$then` and `$else` can themselves be expressions (recursive).
+3 -3
View File
@@ -110,8 +110,8 @@ Use `Pressable` with the built-in `setState` action for interactive UIs like tab
Any prop value can be a data-driven expression resolved at render time:
- **`{ "$path": "/state/key" }`** - reads from data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value
- **`{ "$path": "/state/key" }`** -- reads from data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- conditional value
```json
{
@@ -123,7 +123,7 @@ Any prop value can be a data-driven expression resolved at render time:
}
```
Components receive already-resolved props. No changes needed to component implementations.
Components receive already-resolved props -- no changes needed to component implementations.
## Built-in Actions
+3 -3
View File
@@ -93,8 +93,8 @@ The React schema uses an element tree format:
Any prop value can be a data-driven expression resolved by the renderer before components receive props:
- **`{ "$path": "/state/key" }`** - reads from data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value
- **`{ "$path": "/state/key" }`** -- reads from data model
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** -- conditional value
```json
{
@@ -106,7 +106,7 @@ Any prop value can be a data-driven expression resolved by the renderer before c
}
```
Components receive already-resolved props. No changes needed to component implementations.
Components receive already-resolved props -- no changes needed to component implementations.
## Event System