$npx -y skills add vercel-labs/json-render --skill inkInk terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.
| 1 | # @json-render/ink |
| 2 | |
| 3 | Ink terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props. |
| 4 | |
| 5 | ## Quick Start |
| 6 | |
| 7 | ```typescript |
| 8 | import { defineCatalog } from "@json-render/core"; |
| 9 | import { schema } from "@json-render/ink/schema"; |
| 10 | import { |
| 11 | standardComponentDefinitions, |
| 12 | standardActionDefinitions, |
| 13 | } from "@json-render/ink/catalog"; |
| 14 | import { defineRegistry, Renderer, type Components } from "@json-render/ink"; |
| 15 | import { z } from "zod"; |
| 16 | |
| 17 | // Create catalog with standard + custom components |
| 18 | const catalog = defineCatalog(schema, { |
| 19 | components: { |
| 20 | ...standardComponentDefinitions, |
| 21 | CustomWidget: { |
| 22 | props: z.object({ title: z.string() }), |
| 23 | slots: [], |
| 24 | description: "Custom widget", |
| 25 | }, |
| 26 | }, |
| 27 | actions: standardActionDefinitions, |
| 28 | }); |
| 29 | |
| 30 | // Register only custom components (standard ones are built-in) |
| 31 | const { registry } = defineRegistry(catalog, { |
| 32 | components: { |
| 33 | CustomWidget: ({ props }) => <Text>{props.title}</Text>, |
| 34 | } as Components<typeof catalog>, |
| 35 | }); |
| 36 | |
| 37 | // Render |
| 38 | function App({ spec }) { |
| 39 | return ( |
| 40 | <JSONUIProvider initialState={{}}> |
| 41 | <Renderer spec={spec} registry={registry} /> |
| 42 | </JSONUIProvider> |
| 43 | ); |
| 44 | } |
| 45 | ``` |
| 46 | |
| 47 | ## Spec Structure (Flat Element Map) |
| 48 | |
| 49 | The Ink schema uses a flat element map with a root key: |
| 50 | |
| 51 | ```json |
| 52 | { |
| 53 | "root": "main", |
| 54 | "elements": { |
| 55 | "main": { |
| 56 | "type": "Box", |
| 57 | "props": { "flexDirection": "column", "padding": 1 }, |
| 58 | "children": ["heading", "content"] |
| 59 | }, |
| 60 | "heading": { |
| 61 | "type": "Heading", |
| 62 | "props": { "text": "Dashboard", "level": "h1" }, |
| 63 | "children": [] |
| 64 | }, |
| 65 | "content": { |
| 66 | "type": "Text", |
| 67 | "props": { "text": "Hello from the terminal!" }, |
| 68 | "children": [] |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | ``` |
| 73 | |
| 74 | ## Standard Components |
| 75 | |
| 76 | ### Layout |
| 77 | - `Box` - Flexbox layout container (like a terminal `<div>`). Use for grouping, spacing, borders, alignment. Default flexDirection is row. |
| 78 | - `Text` - Text output with optional styling (color, bold, italic, etc.) |
| 79 | - `Newline` - Inserts blank lines. Must be inside a Box with flexDirection column. |
| 80 | - `Spacer` - Flexible empty space that expands along the main axis. |
| 81 | |
| 82 | ### Content |
| 83 | - `Heading` - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed) |
| 84 | - `Divider` - Horizontal separator with optional centered title |
| 85 | - `Badge` - Colored inline label (variants: default, info, success, warning, error) |
| 86 | - `Spinner` - Animated loading spinner with optional label |
| 87 | - `ProgressBar` - Horizontal progress bar (0-1) |
| 88 | - `Sparkline` - Inline chart using Unicode block characters |
| 89 | - `BarChart` - Horizontal bar chart with labels and values |
| 90 | - `Table` - Tabular data with headers and rows |
| 91 | - `List` - Bulleted or numbered list |
| 92 | - `ListItem` - Structured list row with title, subtitle, leading/trailing text |
| 93 | - `Card` - Bordered container with optional title |
| 94 | - `KeyValue` - Key-value pair display |
| 95 | - `Link` - Clickable URL with optional label |
| 96 | - `StatusLine` - Status message with colored icon (info, success, warning, error) |
| 97 | - `Markdown` - Renders markdown text with terminal styling |
| 98 | |
| 99 | ### Interactive |
| 100 | - `TextInput` - Text input field (events: submit, change) |
| 101 | - `Select` - Selection menu with arrow key navigation (events: change) |
| 102 | - `MultiSelect` - Multi-selection with space to toggle (events: change, submit) |
| 103 | - `ConfirmInput` - Yes/No confirmation prompt (events: confirm, deny) |
| 104 | - `Tabs` - Tab bar navigation with left/right arrow keys (events: change) |
| 105 | |
| 106 | ## Visibility Conditions |
| 107 | |
| 108 | Use `visible` on elements to show/hide based on state. Syntax: `{ "$state": "/path" }`, `{ "$state": "/path", "eq": value }`, `{ "$state": "/path", "not": true }`, `{ "$and": [cond1, cond2] }` for AND, `{ "$or": [cond1, cond2] }` for OR. |
| 109 | |
| 110 | ## Dynamic Prop Expressions |
| 111 | |
| 112 | Any prop value can be a data-driven expression resolved at render time: |
| 113 | |
| 114 | - **`{ "$state": "/state/key" }`** - reads from state model (one-way read) |
| 115 | - **`{ "$bindState": "/path" }`** - two-way binding: use on the natural value prop of form components |
| 116 | - **`{ "$bindItem": "field" }`** - two-way binding to a repeat item field |
| 117 | - **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value |
| 118 | - **`{ "$template": "Hello, ${/name}!" }`** - interpolates state values into strings |
| 119 | |
| 120 | Components do not use a `statePath` prop for two-way binding. Use `{ "$bindState": "/path" }` on the natural value prop instead. |
| 121 | |
| 122 | ## Event System |
| 123 | |
| 124 | Components use `emit` to fire named events. The element's `on` field maps events to action bindings: |
| 125 | |
| 126 | ```tsx |
| 127 | CustomButton: ({ props, emit }) => ( |
| 128 | <Box> |
| 129 | <Text>{props.label}</Text> |
| 130 | {/* emit("press") triggers the action bound in the spec's on.press */} |
| 131 | </Box> |
| 132 | ), |
| 133 | ``` |
| 134 | |
| 135 | ```json |
| 136 | { |
| 137 | "type": "CustomBut |