Compare commits

..
Author SHA1 Message Date
github-actions[bot] 7628640984 chore: version packages (#243)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-23 13:30:11 -05:00
Chris Tate bf3a7ec61d prepare v0.15 release (#242) 2026-03-23 13:23:41 -05:00
Chris Tate 453484985a ink updates (#241)
* ink updates

* no emojis

* design

* fit content

* fixes

* fixes
2026-03-23 13:12:31 -05:00
89 changed files with 738 additions and 120 deletions
+10
View File
@@ -1,5 +1,15 @@
# web
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/codegen@0.15.0
- @json-render/react@0.15.0
- @json-render/yaml@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# example-chat
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
- @json-render/shadcn@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# example-dashboard
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/codegen@0.15.0
- @json-render/react@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+11
View File
@@ -0,0 +1,11 @@
# example-game-engine
## 0.1.1
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
- @json-render/react-three-fiber@0.15.0
- @json-render/yaml@0.15.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-game-engine",
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-image
## 0.1.7
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/image@0.15.0
## 0.1.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -0,0 +1,9 @@
# example-ink-chat
## 0.1.1
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/ink@0.15.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-ink-chat",
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
+81 -31
View File
@@ -85,29 +85,72 @@ function getStepHint(type: string, isLast: boolean): string {
}
// ---------------------------------------------------------------------------
// System prompt — embeds catalog documentation so the AI knows what components
// are available and how to output JSONL patches.
// System prompt — handwritten design guidance + catalog documentation.
// Follows the same pattern as examples/chat/lib/agent.ts: a rich
// AGENT_INSTRUCTIONS string with catalog.prompt() appended at the end.
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT = catalog.prompt({
system:
"You are a terminal chat assistant. Use tools for real-time data (web_search, get_weather, get_hacker_news, get_github_repo, get_crypto_price), then render results as rich terminal UIs.",
const AGENT_INSTRUCTIONS = `You are a terminal assistant that renders polished, information-dense UIs. You call tools for real-time data, then build clean terminal dashboards.
WORKFLOW:
1. Call the appropriate tools to gather real data. Use web_search for topics not covered by the specialized tools (get_weather, get_hacker_news, get_github_repo, get_crypto_price).
2. While tools run, output a single short status line (e.g. "Looking up weather data..."). This is the ONLY text allowed outside the spec fence.
3. After tools return, output ALL content inside a \`\`\`spec fence. Never write paragraphs of prose outside the fence.
4. For simple text replies (greetings, clarifications), still use a \`\`\`spec with a Markdown component.
DESIGN PRINCIPLES:
- HIERARCHY: Every response needs clear visual structure. Start with an h1 Heading for the topic. Use h2 Headings for subsections. Use Card to group related content into shaded areas — Cards render as subtle background fills, not bordered boxes.
- LEAD WITH THE STORY: Open with a brief Markdown paragraph (2-3 sentences) that tells the user the key insight or takeaway. Don't just dump data — frame it.
- SUMMARY METRICS: After the narrative, show 2-4 Metric components for the most important numbers. Metric displays a dim label, bold value, and optional colored trend (up=green, down=red). Group them in a horizontal Box (flexDirection: row, gap: 3) so they read like a dashboard header. Use KeyValue only for simple label:value pairs that don't need emphasis.
- DETAIL SECTIONS: Below the summary, use h2 Headings to introduce each section, followed by a single focused visualization (Table, BarChart, or set of KeyValues).
- ONE REPRESENTATION PER DATA POINT: Never show the same value as both a number and a percentage and a bar. Pick the most meaningful format. Use BarChart with showValues:true OR showPercentage:true, not both.
- TABLES: Always set explicit column widths so columns don't collapse. Use headerColor:"cyan". Keep column headers short (abbreviate if needed). Right-align numeric columns.
- CHARTS: Use distinct colors per bar in BarChart. Good palette: cyan, green, yellow, magenta, blue, red. Use Sparkline for compact inline trends alongside other content.
- COLOR STRATEGY: Use color with intention, not decoration. cyan for labels and headers. green for positive values, growth, success. red for negative values, decline, errors. yellow for warnings or neutral highlights. dimColor:true for secondary/supporting text. Avoid coloring everything — contrast comes from restraint.
- TABLES: Use borderStyle:"single" on Tables for a clean outline. Do NOT put Tables inside Cards — Tables have their own border and don't need additional wrapping.
- SPACING: Use gap:1 between sections. Don't over-pad. Keep the UI compact and scannable. NEVER add padding to the root element — the app already provides outer padding.
- WIDTH: Target 80 columns. Set explicit widths on Tables (total columns should sum to ~70-75). Use wrap:"truncate-end" on Text in tight spaces.
- CALLOUTS: Use Callout for key takeaways, important notes, tips, and warnings. Set type (info/tip/warning/important) for a colored left border accent. Keep content concise — one key point per Callout.
- TIMELINES: Use Timeline for historical events, step-by-step processes, and milestones. Set status per item (completed/current/upcoming) for colored dots. Include dates when available.
- NEVER use emojis anywhere — not in text, labels, titles, table cells, Heading text, or component props. Plain text only.
DASHBOARD PATTERN (use for data-heavy responses):
Root Box (column, gap:1) >
Heading (h1, topic title)
Markdown (2-3 sentence summary with key takeaway)
Box (row, gap:3) > [Metric, Metric, Metric] (top-line metrics, no Card)
Heading (h2, section title)
Table (borderStyle:"single")
Card (title:"Section Name") > BarChart (bar charts go in a titled Card — the Card title replaces h2)
Callout (type:"tip", key takeaway or closing note)
Card wrapping rules: Wrap BarCharts in a Card with a title. Do NOT wrap Metrics or Tables in Cards — Metrics stand alone, Tables have their own border.
COMPARISON PATTERN:
Use BarChart when you want the user to see relative magnitudes at a glance.
Use Table when there are 3+ columns of mixed data types.
Never use both for the same data.
TREND PATTERN:
Use Sparkline for compact inline trend next to a KeyValue.
Use BarChart with year/period labels for detailed time-series.
INTERACTIVITY:
- You can create interactive forms, surveys, and selection interfaces. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.
- ALWAYS include a submit action on interactive UIs. Add a Text or StatusLine telling the user how to submit. Wire submit events to a "submit" action — the app collects form state automatically.
- ALWAYS populate the state field with sensible defaults for all bound values.
- Use $bindState on interactive components for two-way binding. Example: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.
- Use Tabs for multi-section surveys. Use ConfirmInput for yes/no prompts.
- After receiving form data, acknowledge the user's choices meaningfully — don't just echo them back.
${catalog.prompt({
mode: "inline",
customRules: [
"ALL text MUST go inside the spec using the Markdown component. NEVER write prose, summaries, or explanations outside the ```spec fence. The ONLY text allowed outside the fence is a single short status like 'Searching…' while tools run.",
"For text-only answers (greetings, clarifications), still output a ```spec with a Markdown component.",
"Prefer Table for structured data (forecasts, leaderboards, language breakdowns) and KeyValue for label-value pairs (stats, metadata).",
// Interactive components
"INTERACTIVE UIs: You can create interactive forms, surveys, and selection interfaces that the user actually interacts with in the terminal. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.",
"When creating interactive UIs, ALWAYS include a submit action. Add an element (usually a Text or StatusLine) that tells the user to press Enter or a specific key to submit. Wire up the submit event to a 'submit' action — the app will collect the form state and send it back to you automatically.",
"For interactive specs, ALWAYS populate the state field with sensible defaults for all bound values. Example: if a Select binds to /framework, include state: { framework: 'react' }.",
'IMPORTANT: Use $bindState on interactive components so their values are written back to state. Example for Select: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.',
"For surveys/quizzes with multiple sections, use Tabs to organize them. Bind the active tab to state and use visible conditions on children.",
"For confirmation prompts, use ConfirmInput with a clear message. The confirm/deny events can trigger actions.",
"After receiving submitted form data, acknowledge the user's choices and respond to them — don't just repeat what they selected.",
"ALL text MUST go inside the spec using the Markdown component. The ONLY text outside the fence is a short tool-status line.",
"For text-only answers, still output a spec with a Markdown component.",
"Prefer Table for structured data and KeyValue for label-value pairs.",
"NEVER use emojis anywhere in your output. Plain text only.",
],
});
})}`;
// ---------------------------------------------------------------------------
// Types
@@ -236,7 +279,6 @@ function MessageView({ message }: { message: Message }) {
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold>AI:</Text>
{message.spec ? (
<JSONUIProvider initialState={message.spec.state ?? {}}>
<DisableFocus />
@@ -317,9 +359,6 @@ function LiveInteractiveSpec({
if (interactiveKeys.length === 0) {
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold color="green">
AI:
</Text>
<JSONUIProvider store={store} handlers={handlers}>
<Renderer spec={spec} />
</JSONUIProvider>
@@ -331,9 +370,6 @@ function LiveInteractiveSpec({
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold color="green">
AI:
</Text>
{interactiveKeys.length > 1 && (
<Text dimColor>
Step {step + 1} of {interactiveKeys.length}
@@ -417,7 +453,7 @@ export function App() {
try {
const result = streamText({
model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
system: SYSTEM_PROMPT,
system: AGENT_INSTRUCTIONS,
messages: history,
temperature: 0.7,
abortSignal: controller.signal,
@@ -535,11 +571,26 @@ export function App() {
return (
<Box flexDirection="column" padding={1} minHeight={stdout.rows}>
{/* Header */}
<Box marginBottom={1}>
<Text bold>json-render terminal chat</Text>
<Text dimColor> (Ctrl+C to exit)</Text>
<Box marginBottom={1} gap={1}>
<Text bold color="cyan">
json-render
</Text>
<Text dimColor>Ctrl+C to exit</Text>
</Box>
{/* Empty state — show example prompts when no conversation yet */}
{messages.length === 0 && !isStreaming && (
<Box flexDirection="column" marginBottom={1}>
<Text dimColor>Try asking:</Text>
<Box flexDirection="column" paddingLeft={2} marginTop={1} gap={0}>
<Text dimColor>{" weather in tokyo"}</Text>
<Text dimColor>{" top hacker news stories"}</Text>
<Text dimColor>{" tell me about vercel/next.js"}</Text>
<Text dimColor>{" bitcoin price"}</Text>
</Box>
</Box>
)}
{/* Message history — collapsed when interactive wizard is active */}
{liveSpec && !isStreaming ? (
<>
@@ -568,10 +619,9 @@ export function App() {
{/* Live spec preview while streaming */}
{isStreaming && streamingSpec && streamingSpec.root && (
<Box flexDirection="column" marginBottom={1}>
<Text bold>AI:</Text>
<JSONUIProvider initialState={streamingSpec.state ?? {}}>
<DisableFocus />
<Renderer spec={streamingSpec} />
<Renderer spec={streamingSpec} loading />
</JSONUIProvider>
</Box>
)}
+2 -1
View File
@@ -275,7 +275,8 @@ export const getGitHubRepo = tool({
const languageBreakdown = Object.entries(languages)
.map(([lang, bytes]) => ({
language: lang,
percentage: Math.round((bytes / totalBytes) * 100),
percentage:
totalBytes > 0 ? Math.round((bytes / totalBytes) * 100) : 0,
}))
.sort((a, b) => b.percentage - a.percentage)
.slice(0, 6);
+10
View File
@@ -1,5 +1,15 @@
# example-mcp
## 0.1.6
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/mcp@0.15.0
- @json-render/react@0.15.0
- @json-render/shadcn@0.15.0
## 0.1.5
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-mcp",
"version": "0.1.5",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# example-no-ai
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
- @json-render/shadcn@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-react-email
## 0.1.6
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react-email@0.15.0
## 0.1.5
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.5",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-react-native
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react-native@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.9",
"version": "0.1.10",
"private": true,
"main": "expo-router/entry",
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-react-pdf
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react-pdf@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# example-react-three-fiber
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
- @json-render/react-three-fiber@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-three-fiber",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-remotion
## 0.1.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/remotion@0.15.0
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-solid
## 0.1.4
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/solid@0.15.0
## 0.1.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-solid",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"type": "module",
"scripts": {
@@ -1,5 +1,13 @@
# com.example.json-render-demo
## 0.0.11
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
## 0.0.10
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.10",
"version": "0.0.11",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,5 +1,13 @@
# com.example.json-render-fullpage-demo
## 0.0.11
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
## 0.0.10
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.10",
"version": "0.0.11",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",
+8
View File
@@ -1,5 +1,13 @@
# svelte-chat
## 0.0.7
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/svelte@0.15.0
## 0.0.6
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "svelte-chat",
"private": true,
"version": "0.0.6",
"version": "0.0.7",
"type": "module",
"scripts": {
"dev": "vite dev",
+8
View File
@@ -1,5 +1,13 @@
# example-svelte
## 0.1.7
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/svelte@0.15.0
## 0.1.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-svelte",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"type": "module",
"scripts": {
+11
View File
@@ -1,5 +1,16 @@
# vite-renderers
## 0.1.8
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
- @json-render/solid@0.15.0
- @json-render/svelte@0.15.0
- @json-render/vue@0.15.0
## 0.1.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vite-renderers",
"version": "0.1.7",
"version": "0.1.8",
"private": true,
"type": "module",
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-vue
## 0.1.8
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/vue@0.15.0
## 0.1.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-vue",
"version": "0.1.7",
"version": "0.1.8",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
+7
View File
@@ -1,5 +1,12 @@
# @json-render/codegen
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/codegen",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Utilities for generating code from json-render UI trees",
"keywords": [
+12
View File
@@ -1,5 +1,17 @@
# @json-render/core
## 0.15.0
### Minor Changes
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
### New:
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
### Improved:
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/core",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/image
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/image",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
"keywords": [
+18
View File
@@ -0,0 +1,18 @@
# @json-render/ink
## 0.15.0
### Minor Changes
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
### New:
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
### Improved:
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/ink",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
"keywords": [
@@ -57,7 +57,8 @@
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
+77 -11
View File
@@ -259,11 +259,12 @@ export const standardComponentDefinitions = {
borderStyle: z
.enum(["single", "double", "round", "bold", "classic"])
.nullable(),
backgroundColor: z.string().nullable(),
headerColor: z.string().nullable(),
}),
slots: [],
description:
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values.",
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values. Set both borderStyle and backgroundColor together so borders share the same shading.",
example: {
columns: [
{ header: "Name", key: "name", width: 20 },
@@ -306,7 +307,7 @@ export const standardComponentDefinitions = {
example: {
title: "package.json",
subtitle: "Modified 2 hours ago",
leading: "📄",
leading: "*",
trailing: "2.1 KB",
},
},
@@ -314,16 +315,13 @@ export const standardComponentDefinitions = {
Card: {
props: z.object({
title: z.string().nullable(),
borderStyle: z
.enum(["single", "double", "round", "bold", "classic"])
.nullable(),
borderColor: z.string().nullable(),
backgroundColor: z.string().nullable(),
padding: z.number().nullable(),
}),
slots: ["default"],
description:
"Bordered container with optional title. Use for grouping related content with a visual boundary.",
example: { title: "Details", borderStyle: "round", padding: 1 },
"Shaded container with optional title. Renders as a filled background area for grouping related content. Default background is a subtle dark shade.",
example: { title: "Details", padding: 1 },
},
KeyValue: {
@@ -363,6 +361,74 @@ export const standardComponentDefinitions = {
example: { text: "Build completed successfully", status: "success" },
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
detail: z.string().nullable(),
trend: z.enum(["up", "down", "neutral"]).nullable(),
}),
slots: [],
description:
"Key metric display with prominent value and optional trend indicator. Use for important numbers that deserve visual emphasis (price, temperature, stars, market cap).",
example: {
label: "Price",
value: "$70,686",
detail: "24h change",
trend: "up",
},
},
Callout: {
props: z.object({
type: z.enum(["info", "tip", "warning", "important"]).nullable(),
title: z.string().nullable(),
content: z.string(),
}),
slots: [],
description:
"Highlighted callout block with colored left border. Use for key takeaways, tips, warnings, or important notes that should stand out from surrounding content.",
example: {
type: "tip",
title: "Key Takeaway",
content:
"Revenue peaked in FY2022 driven by iPhone 13/14 upgrade cycles.",
},
},
Timeline: {
props: z.object({
items: z.array(
z.object({
title: z.string(),
description: z.string().nullable(),
date: z.string().nullable(),
status: z.enum(["completed", "current", "upcoming"]).nullable(),
}),
),
}),
slots: [],
description:
"Vertical timeline showing ordered events, steps, or milestones. Each item has a status-colored dot, title, optional date, and optional description.",
example: {
items: [
{
title: "Project Started",
description: "Initial commit and setup",
date: "Jan 2024",
status: "completed",
},
{
title: "Beta Release",
description: "Public beta launched",
date: "Mar 2024",
status: "current",
},
{ title: "v1.0", date: "Q2 2024", status: "upcoming" },
],
},
},
// ==========================================================================
// Interactive Components
// ==========================================================================
@@ -468,9 +534,9 @@ export const standardComponentDefinitions = {
"Tab bar navigation. Navigate with left/right arrow keys. Use $bindState on value to bind the active tab to state. Place child content inside and use visible conditions on children to show content for the active tab.",
example: {
tabs: [
{ label: "Overview", value: "overview", icon: "📊" },
{ label: "Logs", value: "logs", icon: "📋" },
{ label: "Settings", value: "settings", icon: "⚙" },
{ label: "Overview", value: "overview" },
{ label: "Logs", value: "logs" },
{ label: "Settings", value: "settings" },
],
},
},
+166 -27
View File
@@ -325,6 +325,7 @@ function TableComponent({ element }: ComponentRenderProps) {
}>;
rows?: Array<Record<string, string>>;
borderStyle?: string;
backgroundColor?: string;
headerColor?: string;
};
@@ -363,7 +364,12 @@ function TableComponent({ element }: ComponentRenderProps) {
| undefined;
return (
<Box flexDirection="column" borderStyle={borderStyleProp}>
<Box
flexDirection="column"
alignSelf="flex-start"
borderStyle={borderStyleProp}
backgroundColor={p.backgroundColor}
>
{/* Header */}
<Box>
{columns.map((col, i) => (
@@ -375,15 +381,19 @@ function TableComponent({ element }: ComponentRenderProps) {
{/* Separator */}
<Text dimColor>{colWidths.map((w) => "─".repeat(w)).join("─")}</Text>
{/* Rows */}
{rows.map((row, rowIdx) => (
<Box key={rowIdx}>
{columns.map((col, i) => (
<Text key={col.key}>
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
</Text>
))}
</Box>
))}
{rows.map((row, rowIdx) => {
const rowKey =
columns.map((c) => row[c.key] ?? "").join("\0") || String(rowIdx);
return (
<Box key={rowKey} gap={0}>
{columns.map((col, i) => (
<Text key={col.key}>
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
</Text>
))}
</Box>
);
})}
</Box>
);
}
@@ -402,7 +412,7 @@ function ListComponent({ element }: ComponentRenderProps) {
return (
<Box flexDirection="column" gap={p.spacing ?? 0}>
{items.map((item, i) => (
<Box key={i} gap={1}>
<Box key={`${i}:${item}`} gap={1}>
<Text dimColor>{p.ordered ? `${i + 1}.` : bullet}</Text>
<Text>{item}</Text>
</Box>
@@ -434,16 +444,15 @@ function ListItemComponent({ element }: ComponentRenderProps) {
function CardComponent({ element, children }: ComponentRenderProps) {
const p = element.props as {
title?: string;
borderStyle?: "single" | "double" | "round" | "bold" | "classic";
borderColor?: string;
backgroundColor?: string;
padding?: number;
};
return (
<Box
flexDirection="column"
borderStyle={p.borderStyle ?? "round"}
borderColor={p.borderColor}
alignSelf="flex-start"
backgroundColor={p.backgroundColor ?? "#1a1a1a"}
padding={p.padding ?? 1}
>
{p.title ? (
@@ -537,10 +546,136 @@ function StatusLineComponent({ element }: ComponentRenderProps) {
);
}
// =============================================================================
// Metric, Callout, Timeline
// =============================================================================
const TREND_CONFIG: Record<string, { prefix: string; color: string }> = {
up: { prefix: "+", color: "green" },
down: { prefix: "", color: "red" },
neutral: { prefix: "~", color: "gray" },
};
function MetricComponent({ element }: ComponentRenderProps) {
const p = element.props as {
label?: string;
value?: string;
detail?: string;
trend?: "up" | "down" | "neutral";
};
const trend = p.trend ? TREND_CONFIG[p.trend] : null;
return (
<Box flexDirection="column">
<Text dimColor>{p.label ?? ""}</Text>
<Box gap={1}>
<Text bold>{p.value ?? ""}</Text>
{trend && p.detail ? (
<Text color={trend.color}>
{trend.prefix}
{p.detail}
</Text>
) : null}
</Box>
{!trend && p.detail ? <Text dimColor>{p.detail}</Text> : null}
</Box>
);
}
const CALLOUT_CONFIG: Record<string, { label: string; color: string }> = {
info: { label: "INFO", color: "blue" },
tip: { label: "TIP", color: "green" },
warning: { label: "WARNING", color: "yellow" },
important: { label: "IMPORTANT", color: "magenta" },
};
function CalloutComponent({ element }: ComponentRenderProps) {
const p = element.props as {
type?: "info" | "tip" | "warning" | "important";
title?: string;
content?: string;
};
const config = CALLOUT_CONFIG[p.type ?? "info"] ?? CALLOUT_CONFIG.info!;
return (
<Box
flexDirection="column"
borderStyle="bold"
borderLeft
borderRight={false}
borderTop={false}
borderBottom={false}
borderColor={config.color}
paddingLeft={1}
>
{p.title ? (
<Text bold color={config.color}>
{p.title}
</Text>
) : null}
<Text>{p.content ?? ""}</Text>
</Box>
);
}
const TIMELINE_DOT: Record<string, { dot: string; color: string }> = {
completed: { dot: "●", color: "green" },
current: { dot: "◆", color: "cyan" },
upcoming: { dot: "○", color: "gray" },
};
function TimelineComponent({ element }: ComponentRenderProps) {
const p = element.props as {
items?: Array<{
title?: string;
description?: string;
date?: string;
status?: "completed" | "current" | "upcoming";
}>;
};
const items = p.items ?? [];
return (
<Box flexDirection="column" gap={1}>
{items.map((item, i) => {
const cfg =
TIMELINE_DOT[item.status ?? "upcoming"] ?? TIMELINE_DOT.upcoming!;
return (
<Box key={i} flexDirection="column">
<Box gap={1}>
<Text color={cfg.color}>{cfg.dot}</Text>
<Text bold>{item.title ?? ""}</Text>
{item.date ? <Text dimColor>{item.date}</Text> : null}
</Box>
{item.description ? (
<Box paddingLeft={2}>
<Text dimColor>{item.description}</Text>
</Box>
) : null}
</Box>
);
})}
</Box>
);
}
// =============================================================================
// Interactive Components
// =============================================================================
/** Count code points instead of UTF-16 code units (handles emoji/surrogate pairs). */
function codePointLength(s: string): number {
return Array.from(s).length;
}
/** Slice by code point index instead of UTF-16 offset. */
function codePointSlice(s: string, start: number, end?: number): string {
return Array.from(s).slice(start, end).join("");
}
function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
const p = element.props as {
placeholder?: string;
@@ -556,11 +691,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
);
const currentValue = value ?? "";
const [cursorPos, setCursorPos] = useState(currentValue.length);
const [cursorPos, setCursorPos] = useState(codePointLength(currentValue));
// Keep cursor in bounds when value changes externally
useEffect(() => {
setCursorPos((prev) => Math.min(prev, currentValue.length));
setCursorPos((prev) => Math.min(prev, codePointLength(currentValue)));
}, [currentValue]);
useInput(
@@ -573,8 +708,8 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
if (key.backspace || key.delete) {
if (cursorPos > 0) {
const newVal =
currentValue.slice(0, cursorPos - 1) +
currentValue.slice(cursorPos);
codePointSlice(currentValue, 0, cursorPos - 1) +
codePointSlice(currentValue, cursorPos);
setValue(newVal);
setCursorPos((prev) => prev - 1);
emit("change");
@@ -588,7 +723,9 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
return;
}
if (key.rightArrow) {
setCursorPos((prev) => Math.min(currentValue.length, prev + 1));
setCursorPos((prev) =>
Math.min(codePointLength(currentValue), prev + 1),
);
return;
}
@@ -598,11 +735,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
if (input) {
const newVal =
currentValue.slice(0, cursorPos) +
codePointSlice(currentValue, 0, cursorPos) +
input +
currentValue.slice(cursorPos);
codePointSlice(currentValue, cursorPos);
setValue(newVal);
setCursorPos((prev) => prev + input.length);
setCursorPos((prev) => prev + codePointLength(input));
emit("change");
}
},
@@ -611,13 +748,12 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
const displayValue = currentValue
? p.mask
? p.mask.repeat(currentValue.length)
? p.mask.repeat(codePointLength(currentValue))
: currentValue
: "";
// Render with cursor position indicator
const before = displayValue.slice(0, cursorPos);
const after = displayValue.slice(cursorPos);
const before = codePointSlice(displayValue, 0, cursorPos);
const after = codePointSlice(displayValue, cursorPos);
return (
<Box gap={1}>
@@ -1178,6 +1314,9 @@ export const standardComponents: ComponentRegistry = {
KeyValue: KeyValueComponent,
Link: LinkComponent,
StatusLine: StatusLineComponent,
Metric: MetricComponent,
Callout: CalloutComponent,
Timeline: TimelineComponent,
TextInput: TextInputComponent,
Select: SelectComponent,
MultiSelect: MultiSelectComponent,
+18 -7
View File
@@ -101,8 +101,8 @@ export interface PendingConfirmation {
export interface ActionContextValue {
/** Registered action handlers */
handlers: Record<string, ActionHandler>;
/** Actions currently executing (for loading indicators) */
loadingActions: Set<string>;
/** Actions currently executing (count of in-flight executions per action name) */
loadingActions: Map<string, number>;
/** Pending confirmation dialog */
pendingConfirmation: PendingConfirmation | null;
/** Execute an action binding */
@@ -141,7 +141,9 @@ export function ActionProvider({
initialHandlersRef.current = initialHandlers;
const navigateRef = useRef(navigate);
navigateRef.current = navigate;
const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
const [loadingActions, setLoadingActions] = useState<Map<string, number>>(
new Map(),
);
const [pendingConfirmation, setPendingConfirmation] =
useState<PendingConfirmation | null>(null);
// Ref tracks current pending confirmation so overlapping confirms can
@@ -267,7 +269,11 @@ export function ActionProvider({
}
const actionName = resolved.action;
setLoadingActions((prev) => new Set(prev).add(actionName));
setLoadingActions((prev) => {
const next = new Map(prev);
next.set(actionName, (next.get(actionName) ?? 0) + 1);
return next;
});
try {
await executeAction({
action: resolved,
@@ -281,8 +287,13 @@ export function ActionProvider({
});
} finally {
setLoadingActions((prev) => {
const next = new Set(prev);
next.delete(actionName);
const next = new Map(prev);
const count = (next.get(actionName) ?? 1) - 1;
if (count <= 0) {
next.delete(actionName);
} else {
next.set(actionName, count);
}
return next;
});
}
@@ -343,7 +354,7 @@ export function useAction(binding: ActionBinding): {
} {
const { execute, loadingActions } = useActions();
const executeAction = useCallback(() => execute(binding), [execute, binding]);
const isLoading = loadingActions.has(binding.action);
const isLoading = (loadingActions.get(binding.action) ?? 0) > 0;
return { execute: executeAction, isLoading };
}
+1 -1
View File
@@ -99,7 +99,7 @@ function parsePatchLine(line: string): ParseResult {
// Recovery: strip trailing extra braces/brackets one at a time
// LLMs commonly generate extra closing characters in nested JSON
let attempt = trimmed;
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 8; i++) {
const last = attempt[attempt.length - 1];
if (last === "}" || last === "]") {
attempt = attempt.slice(0, -1);
+10
View File
@@ -5,6 +5,7 @@ import React, {
useCallback,
useMemo,
} from "react";
import { Text } from "ink";
import type {
UIElement,
Spec,
@@ -268,6 +269,9 @@ const ElementRenderer = React.memo(function ElementRenderer({
console.warn(
`[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`,
);
return (
<Text key={childKey} color="red">{`[Missing: ${childKey}]`}</Text>
);
}
return null;
}
@@ -346,6 +350,12 @@ function RepeatChildren({
console.warn(
`[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`,
);
return (
<Text
key={`${key}:${childKey}`}
color="red"
>{`[Missing: ${childKey}]`}</Text>
);
}
return null;
}
+7
View File
@@ -1,5 +1,12 @@
# @json-render/jotai
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/jotai",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Jotai adapter for json-render StateStore",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/mcp
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/mcp",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/react-email
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-email",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/react-native
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-native",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/react-pdf
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-pdf",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @internal/react-state
## 0.8.10
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.8.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@internal/react-state",
"version": "0.8.9",
"version": "0.8.10",
"private": true,
"license": "Apache-2.0",
"description": "Shared React state context for json-render renderer packages",
+8
View File
@@ -1,5 +1,13 @@
# @json-render/react-three-fiber
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-three-fiber",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/react
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "React renderer for @json-render/core. JSON becomes React components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/redux
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/redux",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Redux adapter for json-render StateStore",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/remotion
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/remotion",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
"keywords": [
+8
View File
@@ -1,5 +1,13 @@
# @json-render/shadcn
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
- @json-render/react@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/shadcn",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/solid
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/solid",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/svelte
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/svelte",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/vue
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/vue",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Vue renderer for @json-render/core. JSON becomes Vue components.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/xstate
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/xstate",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "XState Store adapter for json-render StateStore",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/yaml
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/yaml",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.",
"keywords": [
+7
View File
@@ -1,5 +1,12 @@
# @json-render/zustand
## 0.15.0
### Patch Changes
- Updated dependencies [bf3a7ec]
- @json-render/core@0.15.0
## 0.14.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/zustand",
"version": "0.14.1",
"version": "0.15.0",
"license": "Apache-2.0",
"description": "Zustand adapter for json-render StateStore",
"keywords": [