@json-render/ink
Ink 终端渲染器,可将 JSON 规范转换为交互式终端组件树,支持标准组件、数据绑定、可见性、动作和动态属性。
快速开始
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/ink/schema";
import {
standardComponentDefinitions,
standardActionDefinitions,
} from "@json-render/ink/catalog";
import { defineRegistry, Renderer, type Components } from "@json-render/ink";
import { z } from "zod";
// 创建包含标准组件和自定义组件的目录
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
CustomWidget: {
props: z.object({ title: z.string() }),
slots: [],
description: "自定义组件",
},
},
actions: standardActionDefinitions,
});
// 仅注册自定义组件(标准组件已内置)
const { registry } = defineRegistry(catalog, {
components: {
CustomWidget: ({ props }) => <Text>{props.title}</Text>,
} as Components<typeof catalog>,
});
// 渲染
function App({ spec }) {
return (
<JSONUIProvider initialState={{}}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
);
}
规范结构(平铺元素映射)
Ink 模式使用平铺元素映射,并带有一个根键:
{
"root": "main",
"elements": {
"main": {
"type": "Box",
"props": { "flexDirection": "column", "padding": 1 },
"children": ["heading", "content"]
},
"heading": {
"type": "Heading",
"props": { "text": "仪表盘", "level": "h1" },
"children": []
},
"content": {
"type": "Text",
"props": { "text": "来自终端的问候!" },
"children": []
}
}
}
标准组件
布局
Box- Flexbox 布局容器(类似终端的<div>)。用于分组、间距、边框、对齐。默认 flexDirection 为 row。Text- 文本输出,支持可选样式(颜色、粗体、斜体等)Newline- 插入空行。必须位于 flexDirection 为 column 的 Box 内部。Spacer- 沿主轴伸缩的弹性空白空间。
内容
Heading- 章节标题(h1:粗体+下划线,h2:粗体,h3:粗体+暗淡,h4:暗淡)Divider- 水平分隔线,支持可选居中标题Badge- 彩色内联标签(变体:default, info, success, warning, error)Spinner- 动画加载旋转器,支持可选标签ProgressBar- 水平进度条 (0-1)Sparkline- 使用 Unicode 块字符的内联图表BarChart- 带标签和数值的水平柱状图Table- 带表头和行的表格数据List- 项目符号或编号列表ListItem- 结构化列表行,包含标题、副标题、前导/尾随文本Card- 带可选标题的边框容器KeyValue- 键值对显示Link- 可点击的 URL,支持可选标签StatusLine- 带颜色图标的状态消息(info, success, warning, error)Markdown- 渲染 markdown 文本,应用终端样式
交互式
TextInput- 文本输入字段(事件:submit, change)Select- 支持箭头键导航的选择菜单(事件:change)MultiSelect- 多选,按空格键切换(事件:change, submit)ConfirmInput- 是/否确认提示(事件:confirm, deny)Tabs- 支持左右箭头键切换的选项卡导航栏(事件:change)
可见性条件
在元素上使用 visible 来根据状态显示/隐藏。语法:{ "$state": "/path" },{ "$state": "/path", "eq": value },{ "$state": "/path", "not": true },AND 使用 { "$and": [cond1, cond2] },OR 使用 { "$or": [cond1, cond2] }。
动态属性表达式
任何属性值都可以是在渲染时解析的数据驱动表达式:
{ "$state": "/state/key" }- 从状态模型读取(单向读取){ "$bindState": "/path" }- 双向绑定:用于表单组件的自然值属性{ "$bindItem": "field" }- 双向绑定到重复项字段{ "$cond": <条件>, "$then": <值>, "$else": <值> }- 条件值{ "$template": "你好,${/name}!" }- 将状态值插值到字符串中
组件不应为双向绑定使用 statePath 属性。请改用自然值属性上的 { "$bindState": "/path" }。
事件系统
组件使用 emit 来触发命名事件。元素的 on 字段将事件映射到动作绑定:
CustomButton: ({ props, emit }) => (
<Box>
<Text>{props.label}</Text>
{/* emit("press") 触发规范中 on.press 绑定的动作 */}
</Box>
),
{
"type": "CustomButton",
"props": { "label": "提交" },
"on": { "press": { "action": "submit" } },
"children": []
}
内置动作
setState、pushState 和 removeState 是内置动作,会自动处理:
{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "新建" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
重复(动态列表)
在容器元素上使用 repeat 字段来渲染状态数组中的项目:
{
"type": "Box",
"props": { "flexDirection": "column" },
"repeat": { "statePath": "/items", "key": "id" },
"children": ["item-row"]
}
在重复的子元素中,使用 { "$item": "field" } 读取当前项目,使用 { "$index": true } 获取当前索引。
流式渲染
使用 useUIStream 从 JSONL 补丁流中逐步渲染规范:
import { useUIStream } from "@json-render/ink";
const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });
服务端提示生成
使用 ./server 导出从您的目录生成 AI 系统提示:
import { catalog } from "./catalog";
const systemPrompt = catalog.prompt({ system: "你是一个终端助手。" });
提供者
| 提供者 | 用途 |
|---|---|
StateProvider | 跨组件共享状态(JSON Pointer 路径)。接受可选的 store 属性用于受控模式。 |
ActionProvider | 处理通过事件系统触发的动作 |
VisibilityProvider | 基于状态启用条件渲染 |
ValidationProvider | 表单字段验证 |
FocusProvider | 管理交互式组件间的焦点 |
JSONUIProvider | 所有上下文的组合提供者 |
外部存储(受控模式)
将 StateStore 传递给 StateProvider(或 JSONUIProvider)以使用外部状态管理:
import { createStateStore, type StateStore } from "@json-render/ink";
const store = createStateStore({ count: 0 });
<StateProvider store={store}>{children}</StateProvider>
store.set("/count", 1); // React 会自动重新渲染
当提供了 store 时,initialState 和 onStateChange 将被忽略。
createRenderer(更高级的 API)
import { createRenderer } from "@json-render/ink";
import { standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";
const InkRenderer = createRenderer(catalog, {
...standardComponents,
// 在此处覆盖自定义组件
});
// InkRenderer 包含所有提供者(状态、可见性、动作、焦点)
render(
<InkRenderer spec={spec} state={{ activeTab: "overview" }} />
);
关键导出
| 导出 | 用途 |
|---|---|
defineRegistry | 从目录创建类型安全的组件注册表 |
Renderer | 使用注册表渲染规范 |
createRenderer | 更高级别:创建带有内置提供者的组件 |
JSONUIProvider | 所有上下文的组合提供者 |
schema | Ink 平铺元素映射模式(包含内置状态动作) |
standardComponentDefinitions | 所有标准组件的目录定义 |
standardActionDefinitions | 标准动作的目录定义 |
standardComponents | 预构建的组件实现 |
useStateStore | 访问状态上下文 |
useStateValue | 从状态获取单个值 |
useBoundProp | $bindState/$bindItem 表达式的双向绑定 |
useActions | 访问动作上下文 |
useAction | 获取单个动作调度函数 |
useOptionalValidation | useValidation 的非抛出变体 |
useUIStream | 从 API 端点流式获取规范 |
createStateStore | 创建与框架无关的内存 StateStore |
StateStore | 用于插入外部状态管理的接口 |
Components | 类型化的组件映射(目录感知) |
Actions | 类型化的动作映射(目录感知) |
ComponentContext | 类型化的组件上下文(目录感知) |
flatToTree | 将平铺元素映射转换为树形结构 |
终端 UI 设计指南
- 使用 Box 进行布局(flexDirection, padding, gap)。默认 flexDirection 为 row。
- 终端宽度约为 80-120 列。主结构建议使用垂直布局(flexDirection: column)。
- 在 Box 上使用 borderStyle 进行视觉分组(single, double, round, bold)。
- 使用命名的终端颜色:red, green, yellow, blue, magenta, cyan, white, gray。
- 使用 Heading 作为章节标题,Divider 分隔章节,Badge 表示状态,KeyValue 显示标签数据,Card 表示带边框的分组。
- 使用 Tabs 实现多视图 UI,并在子内容上使用可见性条件。
- 使用 Sparkline 显示内联趋势,BarChart 比较数值。