Skip to content

Commit d69a59e

Browse files
authored
feat: add @json-render/ink terminal renderer (#240)
* feat: add @json-render/ink terminal renderer and ink-chat example Adds a new `@json-render/ink` package that brings json-render specs to terminal UIs via Ink (React for CLIs). Includes 24 standard components (layout, text, inputs, markdown, etc.), context providers for state, validation, visibility, actions, focus, and repeat scopes, plus a streaming JSONL hook for progressive spec rendering. Also adds an `ink-chat` example app demonstrating an AI chat interface in the terminal with streaming responses, tool calls, and interactive wizard flows. Includes 76 tests (24 unit + 52 e2e), docs page, and web app integration. * feat: show live spec preview during streaming Render the spec progressively as JSONL patches arrive instead of only showing a spinner. The preview disappears when streaming completes and the finalized message replaces it in history. * feat: clip streaming preview to 6 lines * refactor: move spinner into input box, show full streaming preview * fix: skip spacer during streaming to prevent clipping tall previews * fix: strip invisible colors on dark terminal backgrounds Drop foreground colors like "black" and "#000000" from AI-generated specs so text remains readable on dark terminals. Removed hardcoded color="black" from Badge and added safeColor() guard to all components that accept user-specified color props. * fix: always show spacer to keep input pinned to bottom * fix: show dash placeholder for empty values in Table and KeyValue Empty or null values in Table cells and KeyValue now render as "—" instead of blank space. Also removed dimColor from ListItem subtitle and trailing for better readability on dark terminals. * fix: division by zero in Sparkline sampling when maxWidth is 1 * fix: Review feedback Signed-off-by: Alexis Rico <sferadev@gmail.com> * fix: Update readme Signed-off-by: Alexis Rico <sferadev@gmail.com> --------- Signed-off-by: Alexis Rico <sferadev@gmail.com>
1 parent c43b36e commit d69a59e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+8086
-11
lines changed

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"@json-render/svelte",
2323
"@json-render/solid",
2424
"@json-render/react-three-fiber",
25-
"@json-render/yaml"
25+
"@json-render/yaml",
26+
"@json-render/ink"
2627
]
2728
],
2829
"linked": [],

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ npm install @json-render/core @json-render/vue
2323
npm install @json-render/core @json-render/svelte
2424
# or for SolidJS
2525
npm install @json-render/core @json-render/solid
26+
# or for terminal UIs
27+
npm install @json-render/core @json-render/ink ink react
2628
# or for 3D scenes
2729
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
2830
```
@@ -128,6 +130,7 @@ function Dashboard({ spec }) {
128130
| `@json-render/remotion` | Remotion video renderer, timeline schema |
129131
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
130132
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
133+
| `@json-render/ink` | Ink terminal renderer with built-in components for interactive TUIs. |
131134
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
132135
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
133136
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
@@ -479,6 +482,47 @@ const { registry } = defineRegistry(catalog, {
479482
/>;
480483
```
481484

485+
### Ink (Terminal)
486+
487+
```tsx
488+
import { defineCatalog } from "@json-render/core";
489+
import {
490+
schema,
491+
standardComponentDefinitions,
492+
standardActionDefinitions,
493+
defineRegistry,
494+
Renderer,
495+
JSONUIProvider,
496+
} from "@json-render/ink";
497+
498+
const catalog = defineCatalog(schema, {
499+
components: { ...standardComponentDefinitions },
500+
actions: standardActionDefinitions,
501+
});
502+
503+
const { registry } = defineRegistry(catalog, { components: {} });
504+
505+
const spec = {
506+
root: "card-1",
507+
elements: {
508+
"card-1": {
509+
type: "Card",
510+
props: { title: "Status" },
511+
children: ["status-1"],
512+
},
513+
"status-1": {
514+
type: "StatusLine",
515+
props: { label: "Build", status: "success" },
516+
children: [],
517+
},
518+
},
519+
};
520+
521+
<JSONUIProvider initialState={{}}>
522+
<Renderer spec={spec} registry={registry} />
523+
</JSONUIProvider>;
524+
```
525+
482526
## Features
483527

484528
### Streaming (SpecStream)
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { pageMetadata } from "@/lib/page-metadata"
2+
export const metadata = pageMetadata("docs/api/ink")
3+
4+
# @json-render/ink
5+
6+
Terminal renderer for [Ink](https://github.com/vadimdemedes/ink) with multiple standard components, providers, hooks, and streaming support.
7+
8+
## Installation
9+
10+
<PackageInstall packages="@json-render/core @json-render/ink" />
11+
12+
Peer dependencies: `react ^18.0.0 || ^19.0.0`, `ink ^6.0.0`, and `zod ^4.0.0`.
13+
14+
<PackageInstall packages="react ink zod" />
15+
16+
## Standard Components
17+
18+
### Layout
19+
20+
<table>
21+
<thead>
22+
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
23+
</thead>
24+
<tbody>
25+
<tr><td><code>Box</code></td><td><code>flexDirection</code>, <code>alignItems</code>, <code>justifyContent</code>, <code>gap</code>, <code>padding</code>, <code>margin</code>, <code>borderStyle</code>, <code>borderColor</code>, <code>width</code>, <code>height</code>, <code>display</code>, <code>overflow</code></td><td>Flexbox layout container (like a terminal div)</td></tr>
26+
<tr><td><code>Spacer</code></td><td>(none)</td><td>Flexible empty space that expands to fill available room</td></tr>
27+
<tr><td><code>Newline</code></td><td><code>count</code></td><td>Insert blank lines</td></tr>
28+
</tbody>
29+
</table>
30+
31+
### Content
32+
33+
<table>
34+
<thead>
35+
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
36+
</thead>
37+
<tbody>
38+
<tr><td><code>Text</code></td><td><code>text</code>, <code>color</code>, <code>bold</code>, <code>italic</code>, <code>underline</code>, <code>strikethrough</code>, <code>dimColor</code>, <code>inverse</code>, <code>wrap</code></td><td>Text output with styling</td></tr>
39+
<tr><td><code>Heading</code></td><td><code>text</code>, <code>level</code> (h1-h4), <code>color</code></td><td>Section heading</td></tr>
40+
<tr><td><code>Divider</code></td><td><code>character</code>, <code>color</code>, <code>dimColor</code>, <code>title</code>, <code>width</code></td><td>Horizontal separator line with optional title</td></tr>
41+
<tr><td><code>Badge</code></td><td><code>label</code>, <code>variant</code></td><td>Colored inline label (default, info, success, warning, error)</td></tr>
42+
<tr><td><code>Spinner</code></td><td><code>label</code>, <code>color</code></td><td>Animated loading spinner</td></tr>
43+
<tr><td><code>ProgressBar</code></td><td><code>progress</code> (0-1), <code>width</code>, <code>color</code>, <code>label</code></td><td>Horizontal progress bar</td></tr>
44+
<tr><td><code>StatusLine</code></td><td><code>text</code>, <code>status</code>, <code>icon</code></td><td>Status message with colored icon</td></tr>
45+
<tr><td><code>KeyValue</code></td><td><code>label</code>, <code>value</code>, <code>labelColor</code>, <code>separator</code></td><td>Key-value pair display</td></tr>
46+
<tr><td><code>Link</code></td><td><code>url</code>, <code>label</code>, <code>color</code></td><td>Renders a URL as underlined text. Shows "label (url)" when label is provided.</td></tr>
47+
<tr><td><code>Markdown</code></td><td><code>text</code></td><td>Renders markdown with terminal styling (headings, bold, italic, code, lists, blockquotes, horizontal rules)</td></tr>
48+
</tbody>
49+
</table>
50+
51+
### Data
52+
53+
<table>
54+
<thead>
55+
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
56+
</thead>
57+
<tbody>
58+
<tr><td><code>Table</code></td><td><code>columns</code>, <code>rows</code>, <code>borderStyle</code>, <code>headerColor</code></td><td>Tabular data with headers</td></tr>
59+
<tr><td><code>List</code></td><td><code>items</code>, <code>ordered</code>, <code>bulletChar</code>, <code>spacing</code></td><td>Bulleted or numbered list</td></tr>
60+
<tr><td><code>ListItem</code></td><td><code>title</code>, <code>subtitle</code>, <code>leading</code>, <code>trailing</code></td><td>Structured list row</td></tr>
61+
<tr><td><code>Card</code></td><td><code>title</code>, <code>borderStyle</code>, <code>borderColor</code>, <code>padding</code></td><td>Bordered container with optional title</td></tr>
62+
<tr><td><code>Sparkline</code></td><td><code>data</code>, <code>width</code>, <code>color</code>, <code>label</code>, <code>min</code>, <code>max</code></td><td>Inline sparkline chart using Unicode blocks (▁▂▃▄▅▆▇█)</td></tr>
63+
<tr><td><code>BarChart</code></td><td><code>data</code> (label/value/color), <code>width</code>, <code>showValues</code>, <code>showPercentage</code></td><td>Horizontal bar chart for comparing values</td></tr>
64+
</tbody>
65+
</table>
66+
67+
### Interactive
68+
69+
<table>
70+
<thead>
71+
<tr><th>Component</th><th>Props</th><th>Description</th></tr>
72+
</thead>
73+
<tbody>
74+
<tr><td><code>TextInput</code></td><td><code>placeholder</code>, <code>value</code> (use <code>$bindState</code>), <code>label</code>, <code>mask</code></td><td>Text input field. Press Enter to submit.</td></tr>
75+
<tr><td><code>Select</code></td><td><code>options</code>, <code>value</code> (use <code>$bindState</code>), <code>label</code></td><td>Arrow-key selection menu</td></tr>
76+
<tr><td><code>MultiSelect</code></td><td><code>options</code>, <code>value</code> (use <code>$bindState</code>), <code>label</code>, <code>min</code>, <code>max</code></td><td>Multi-selection menu. Space to toggle, Enter to confirm.</td></tr>
77+
<tr><td><code>ConfirmInput</code></td><td><code>message</code>, <code>defaultValue</code>, <code>yesLabel</code>, <code>noLabel</code></td><td>Yes/No confirmation prompt. Press Y or N.</td></tr>
78+
<tr><td><code>Tabs</code></td><td><code>tabs</code>, <code>value</code> (use <code>$bindState</code>), <code>color</code></td><td>Tab bar navigation with left/right arrow keys. Place child content inside with visible conditions.</td></tr>
79+
</tbody>
80+
</table>
81+
82+
## Providers
83+
84+
### JSONUIProvider
85+
86+
Convenience wrapper around all providers: `StateProvider``VisibilityProvider``ValidationProvider``ActionProvider``FocusProvider`.
87+
88+
```tsx
89+
import { JSONUIProvider, Renderer } from "@json-render/ink";
90+
91+
<JSONUIProvider initialState={{}} handlers={handlers}>
92+
<Renderer spec={spec} registry={registry} />
93+
</JSONUIProvider>
94+
```
95+
96+
### StateProvider
97+
98+
```tsx
99+
<StateProvider initialState={object} onStateChange={fn}>
100+
{children}
101+
</StateProvider>
102+
```
103+
104+
<table>
105+
<thead>
106+
<tr><th>Prop</th><th>Type</th><th>Description</th></tr>
107+
</thead>
108+
<tbody>
109+
<tr><td><code>store</code></td><td><code>StateStore</code></td><td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td></tr>
110+
<tr><td><code>initialState</code></td><td><code>Record&lt;string, unknown&gt;</code></td><td>Initial state model (uncontrolled mode).</td></tr>
111+
<tr><td><code>onStateChange</code></td><td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td><td>Callback when state changes (uncontrolled mode).</td></tr>
112+
</tbody>
113+
</table>
114+
115+
#### External Store (Controlled Mode)
116+
117+
Pass a `StateStore` to bypass internal state and wire json-render to any state management:
118+
119+
```tsx
120+
import { createStateStore } from "@json-render/ink";
121+
122+
const store = createStateStore({ count: 0 });
123+
124+
<StateProvider store={store}>
125+
{children}
126+
</StateProvider>
127+
128+
// Mutate from anywhere — components re-render automatically:
129+
store.set("/count", 1);
130+
```
131+
132+
The `store` prop is also available on `JSONUIProvider` and `createRenderer`.
133+
134+
### ActionProvider
135+
136+
```tsx
137+
<ActionProvider handlers={Record<string, ActionHandler>} navigate={fn}>
138+
{children}
139+
</ActionProvider>
140+
```
141+
142+
Built-in actions: `setState`, `pushState`, `removeState`, `log`, `exit`. Custom handlers override built-ins. Includes a terminal confirmation dialog (press Y/N) for actions with `confirm`.
143+
144+
### VisibilityProvider
145+
146+
```tsx
147+
<VisibilityProvider>
148+
{children}
149+
</VisibilityProvider>
150+
```
151+
152+
### ValidationProvider
153+
154+
```tsx
155+
<ValidationProvider>
156+
{children}
157+
</ValidationProvider>
158+
```
159+
160+
### FocusProvider
161+
162+
```tsx
163+
<FocusProvider>
164+
{children}
165+
</FocusProvider>
166+
```
167+
168+
Manages Tab-cycling focus between interactive components (TextInput, Select). Supports `useFocusDisable` to suppress cycling during modal dialogs.
169+
170+
## defineRegistry
171+
172+
Create a type-safe component registry. Standard components are built-in; only register custom components.
173+
174+
```tsx
175+
import { defineRegistry, type Components } from "@json-render/ink";
176+
177+
const { registry, handlers, executeAction } = defineRegistry(catalog, {
178+
components: {
179+
MyWidget: ({ props }) => <Text>{props.label}</Text>,
180+
} as Components<typeof catalog>,
181+
actions: {
182+
submit: async (params, setState, state) => {
183+
// custom action logic
184+
},
185+
},
186+
});
187+
```
188+
189+
`handlers` is designed for `JSONUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
190+
191+
## createRenderer
192+
193+
Higher-level helper that wraps `Renderer` + all providers into a single component.
194+
195+
```tsx
196+
import { createRenderer } from "@json-render/ink";
197+
198+
const UIRenderer = createRenderer(catalog, components);
199+
200+
<UIRenderer spec={spec} state={initialState} />;
201+
```
202+
203+
## Hooks
204+
205+
### useUIStream
206+
207+
```typescript
208+
const {
209+
spec, // Spec | null - current UI state
210+
isStreaming, // boolean - true while streaming
211+
error, // Error | null
212+
send, // (prompt: string, context?: Record<string, unknown>) => Promise<void>
213+
stop, // () => void - abort the current stream
214+
clear, // () => void - reset spec and error
215+
} = useUIStream({
216+
api: string,
217+
onComplete?: (spec: Spec) => void,
218+
onError?: (error: Error) => void,
219+
fetch?: (url: string, init?: RequestInit) => Promise<Response>,
220+
validate?: boolean,
221+
maxRetries?: number,
222+
});
223+
```
224+
225+
### useStateStore
226+
227+
```typescript
228+
const { state, get, set, update } = useStateStore();
229+
```
230+
231+
### useStateValue
232+
233+
```typescript
234+
const value = useStateValue(path: string);
235+
```
236+
237+
### useBoundProp
238+
239+
```typescript
240+
const [value, setValue] = useBoundProp(resolvedValue, bindingPath);
241+
```
242+
243+
### useActions
244+
245+
```typescript
246+
const { execute } = useActions();
247+
```
248+
249+
### useIsVisible
250+
251+
```typescript
252+
const isVisible = useIsVisible(condition?: VisibilityCondition);
253+
```
254+
255+
### useFocus
256+
257+
```typescript
258+
const { isActive, id } = useFocus();
259+
```
260+
261+
### useFocusDisable
262+
263+
```typescript
264+
useFocusDisable(disabled: boolean);
265+
```
266+
267+
Suppresses Tab-cycling while `disabled` is true (e.g., during a modal dialog).
268+
269+
## Catalog Exports
270+
271+
```typescript
272+
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/ink/catalog";
273+
import { schema } from "@json-render/ink/schema";
274+
```
275+
276+
<table>
277+
<thead>
278+
<tr><th>Export</th><th>Purpose</th></tr>
279+
</thead>
280+
<tbody>
281+
<tr><td><code>standardComponentDefinitions</code></td><td>Catalog definitions for all 19 standard components</td></tr>
282+
<tr><td><code>standardActionDefinitions</code></td><td>Catalog definitions for standard actions (setState, pushState, removeState, log, exit)</td></tr>
283+
<tr><td><code>schema</code></td><td>Ink element tree schema</td></tr>
284+
</tbody>
285+
</table>
286+
287+
## Server Export
288+
289+
```typescript
290+
import { schema, standardComponentDefinitions, standardActionDefinitions } from "@json-render/ink/server";
291+
```
292+
293+
Re-exports the schema and catalog definitions for server-side usage (e.g., building system prompts).

0 commit comments

Comments
 (0)