@json-render/ink
Renderizador de terminal Ink que convierte especificaciones JSON en árboles de componentes de terminal interactivos con componentes estándar, enlace de datos, visibilidad, acciones y propiedades dinámicas.
Inicio Rápido
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";
// Crear catálogo con componentes estándar + personalizados
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
CustomWidget: {
props: z.object({ title: z.string() }),
slots: [],
description: "Widget personalizado",
},
},
actions: standardActionDefinitions,
});
// Registrar solo componentes personalizados (los estándar vienen integrados)
const { registry } = defineRegistry(catalog, {
components: {
CustomWidget: ({ props }) => <Text>{props.title}</Text>,
} as Components<typeof catalog>,
});
// Renderizar
function App({ spec }) {
return (
<JSONUIProvider initialState={{}}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
);
}
Estructura de la Especificación (Mapa Plano de Elementos)
El esquema Ink utiliza un mapa plano de elementos con una clave raíz:
{
"root": "main",
"elements": {
"main": {
"type": "Box",
"props": { "flexDirection": "column", "padding": 1 },
"children": ["heading", "content"]
},
"heading": {
"type": "Heading",
"props": { "text": "Dashboard", "level": "h1" },
"children": []
},
"content": {
"type": "Text",
"props": { "text": "¡Hola desde la terminal!" },
"children": []
}
}
}
Componentes Estándar
Diseño
Box- Contenedor de diseño Flexbox (como un<div>de terminal). Útil para agrupar, espaciar, bordes y alineación. ElflexDirectionpor defecto esrow.Text- Salida de texto con estilo opcional (color, negrita, cursiva, etc.)Newline- Inserta líneas en blanco. Debe estar dentro de unBoxconflexDirectioncolumn.Spacer- Espacio vacío flexible que se expande a lo largo del eje principal.
Contenido
Heading- Encabezado de sección (h1: negrita+subrayado, h2: negrita, h3: negrita+atenuado, h4: atenuado)Divider- Separador horizontal con título centrado opcionalBadge- Etiqueta en línea de color (variantes: default, info, success, warning, error)Spinner- Indicador de carga animado con etiqueta opcionalProgressBar- Barra de progreso horizontal (0-1)Sparkline- Gráfico en línea usando caracteres Unicode de bloqueBarChart- Gráfico de barras horizontal con etiquetas y valoresTable- Datos tabulares con encabezados y filasList- Lista con viñetas o numeradaListItem- Fila de lista estructurada con título, subtítulo, texto inicial/finalCard- Contenedor con borde y título opcionalKeyValue- Visualización de par clave-valorLink- Enlace clickeable con etiqueta opcionalStatusLine- Mensaje de estado con ícono de color (info, success, warning, error)Markdown- Renderiza texto Markdown con estilo de terminal
Interactivos
TextInput- Campo de entrada de texto (eventos: submit, change)Select- Menú de selección con navegación por teclas de flecha (evento: change)MultiSelect- Selección múltiple con espacio para alternar (eventos: change, submit)ConfirmInput- Solicitud de confirmación Sí/No (eventos: confirm, deny)Tabs- Navegación por pestañas con teclas de flecha izquierda/derecha (evento: change)
Condiciones de Visibilidad
Usa visible en los elementos para mostrar/ocultar según el estado. Sintaxis: { "$state": "/path" }, { "$state": "/path", "eq": valor }, { "$state": "/path", "not": true }, { "$and": [cond1, cond2] } para Y, { "$or": [cond1, cond2] } para O.
Expresiones de Propiedades Dinámicas
Cualquier valor de propiedad puede ser una expresión impulsada por datos resuelta en tiempo de renderizado:
{ "$state": "/state/key" }- lee del modelo de estado (lectura unidireccional){ "$bindState": "/path" }- enlace bidireccional: úsalo en la propiedad de valor natural de componentes de formulario{ "$bindItem": "campo" }- enlace bidireccional a un campo de elemento repetido{ "$cond": <condición>, "$then": <valor>, "$else": <valor> }- valor condicional{ "$template": "¡Hola, ${/name}!" }- interpola valores de estado en cadenas
Los componentes no usan una propiedad statePath para enlace bidireccional. Usa { "$bindState": "/path" } en la propiedad de valor natural en su lugar.
Sistema de Eventos
Los componentes usan emit para disparar eventos con nombre. El campo on del elemento mapea eventos a enlaces de acción:
CustomButton: ({ props, emit }) => (
<Box>
<Text>{props.label}</Text>
{/* emit("press") dispara la acción vinculada en on.press de la especificación */}
</Box>
),
{
"type": "CustomButton",
"props": { "label": "Enviar" },
"on": { "press": { "action": "submit" } },
"children": []
}
Acciones Integradas
setState, pushState y removeState están integradas y se manejan automáticamente:
{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "Nuevo" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
Repetición (Listas Dinámicas)
Usa el campo repeat en un elemento contenedor para renderizar elementos desde un arreglo de estado:
{
"type": "Box",
"props": { "flexDirection": "column" },
"repeat": { "statePath": "/items", "key": "id" },
"children": ["item-row"]
}
Dentro de los hijos repetidos, usa { "$item": "campo" } para leer del elemento actual y { "$index": true } para el índice actual.
Transmisión (Streaming)
Usa useUIStream para renderizar progresivamente especificaciones desde flujos de parches JSONL:
import { useUIStream } from "@json-render/ink";
const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });
Generación de Prompts del Lado del Servidor
Usa la exportación ./server para generar prompts de sistema AI desde tu catálogo:
import { catalog } from "./catalog";
const systemPrompt = catalog.prompt({ system: "Eres un asistente de terminal." });
Proveedores
| Proveedor | Propósito |
|---|---|
StateProvider | Compartir estado entre componentes (rutas JSON Pointer). Acepta la propiedad opcional store para modo controlado. |
ActionProvider | Manejar acciones enviadas a través del sistema de eventos |
VisibilityProvider | Habilitar renderizado condicional basado en el estado |
ValidationProvider | Validación de campos de formulario |
FocusProvider | Gestionar el foco en componentes interactivos |
JSONUIProvider | Proveedor combinado para todos los contextos |
Almacenamiento Externo (Modo Controlado)
Pasa un StateStore a StateProvider (o JSONUIProvider) para usar gestión de estado externa:
import { createStateStore, type StateStore } from "@json-render/ink";
const store = createStateStore({ count: 0 });
<StateProvider store={store}>{children}</StateProvider>
store.set("/count", 1); // React se vuelve a renderizar automáticamente
Cuando se proporciona store, se ignoran initialState y onStateChange.
createRenderer (API de Alto Nivel)
import { createRenderer } from "@json-render/ink";
import { standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";
const InkRenderer = createRenderer(catalog, {
...standardComponents,
// anulación de componentes personalizados aquí
});
// InkRenderer incluye todos los proveedores (estado, visibilidad, acciones, foco)
render(
<InkRenderer spec={spec} state={{ activeTab: "overview" }} />
);
Exportaciones Clave
| Exportación | Propósito |
|---|---|
defineRegistry | Crear un registro de componentes con seguridad de tipos a partir de un catálogo |
Renderer | Renderizar una especificación usando un registro |
createRenderer | Alto nivel: crea un componente con proveedores integrados |
JSONUIProvider | Proveedor combinado para todos los contextos |
schema | Esquema de mapa plano de elementos Ink (incluye acciones de estado integradas) |
standardComponentDefinitions | Definiciones del catálogo para todos los componentes estándar |
standardActionDefinitions | Definiciones del catálogo para acciones estándar |
standardComponents | Implementaciones de componentes preconstruidos |
useStateStore | Acceder al contexto de estado |
useStateValue | Obtener un valor único del estado |
useBoundProp | Enlace bidireccional para expresiones $bindState/$bindItem |
useActions | Acceder al contexto de acciones |
useAction | Obtener una función de despacho de acción única |
useOptionalValidation | Variante que no lanza excepción de useValidation |
useUIStream | Transmitir especificaciones desde un endpoint de API |
createStateStore | Crear un StateStore en memoria independiente del framework |
StateStore | Interfaz para conectar gestión de estado externa |
Components | Mapa de componentes tipado (consciente del catálogo) |
Actions | Mapa de acciones tipado (consciente del catálogo) |
ComponentContext | Contexto de componentes tipado (consciente del catálogo) |
flatToTree | Convertir mapa plano de elementos a estructura de árbol |
Directrices de Diseño de UI de Terminal
- Usa Box para diseño (flexDirection, padding, gap). El
flexDirectionpor defecto esrow. - El ancho de la terminal es de ~80-120 columnas. Prefiere diseños verticales (flexDirection: column) para la estructura principal.
- Usa borderStyle en Box para agrupación visual (simple, doble, redondeado, negrita).
- Usa colores de terminal con nombre: rojo, verde, amarillo, azul, magenta, cian, blanco, gris.
- Usa Heading para títulos de sección, Divider para separar secciones, Badge para estado, KeyValue para datos etiquetados, Card para grupos con borde.
- Usa Tabs para UIs multivista con condiciones de visibilidad en el contenido hijo.
- Usa Sparkline para tendencias en línea y BarChart para comparar valores.