skills/dev-workflow/ink/SKILL.md
Ink 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.
npx skillsauth add The-Utopia-Studio/skills inkInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ink terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.
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";
// Create catalog with standard + custom components
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
CustomWidget: {
props: z.object({ title: z.string() }),
slots: [],
description: "Custom widget",
},
},
actions: standardActionDefinitions,
});
// Register only custom components (standard ones are built-in)
const { registry } = defineRegistry(catalog, {
components: {
CustomWidget: ({ props }) => <Text>{props.title}</Text>,
} as Components<typeof catalog>,
});
// Render
function App({ spec }) {
return (
<JSONUIProvider initialState={{}}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
);
}
The Ink schema uses a flat element map with a root key:
{
"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": "Hello from the terminal!" },
"children": []
}
}
}
Box - Flexbox layout container (like a terminal <div>). Use for grouping, spacing, borders, alignment. Default flexDirection is row.Text - Text output with optional styling (color, bold, italic, etc.)Newline - Inserts blank lines. Must be inside a Box with flexDirection column.Spacer - Flexible empty space that expands along the main axis.Heading - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)Divider - Horizontal separator with optional centered titleBadge - Colored inline label (variants: default, info, success, warning, error)Spinner - Animated loading spinner with optional labelProgressBar - Horizontal progress bar (0-1)Sparkline - Inline chart using Unicode block charactersBarChart - Horizontal bar chart with labels and valuesTable - Tabular data with headers and rowsList - Bulleted or numbered listListItem - Structured list row with title, subtitle, leading/trailing textCard - Bordered container with optional titleKeyValue - Key-value pair displayLink - Clickable URL with optional labelStatusLine - Status message with colored icon (info, success, warning, error)Markdown - Renders markdown text with terminal stylingTextInput - Text input field (events: submit, change)Select - Selection menu with arrow key navigation (events: change)MultiSelect - Multi-selection with space to toggle (events: change, submit)ConfirmInput - Yes/No confirmation prompt (events: confirm, deny)Tabs - Tab bar navigation with left/right arrow keys (events: change)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.
Any prop value can be a data-driven expression resolved at render time:
{ "$state": "/state/key" } - reads from state model (one-way read){ "$bindState": "/path" } - two-way binding: use on the natural value prop of form components{ "$bindItem": "field" } - two-way binding to a repeat item field{ "$cond": <condition>, "$then": <value>, "$else": <value> } - conditional value{ "$template": "Hello, ${/name}!" } - interpolates state values into stringsComponents do not use a statePath prop for two-way binding. Use { "$bindState": "/path" } on the natural value prop instead.
Components use emit to fire named events. The element's on field maps events to action bindings:
CustomButton: ({ props, emit }) => (
<Box>
<Text>{props.label}</Text>
{/* emit("press") triggers the action bound in the spec's on.press */}
</Box>
),
{
"type": "CustomButton",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit" } },
"children": []
}
setState, pushState, and removeState are built-in and handled automatically:
{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } }
{ "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } }
{ "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
Use the repeat field on a container element to render items from a state array:
{
"type": "Box",
"props": { "flexDirection": "column" },
"repeat": { "statePath": "/items", "key": "id" },
"children": ["item-row"]
}
Inside repeated children, use { "$item": "field" } to read from the current item and { "$index": true } for the current index.
Use useUIStream to progressively render specs from JSONL patch streams:
import { useUIStream } from "@json-render/ink";
const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });
Use the ./server export to generate AI system prompts from your catalog:
import { catalog } from "./catalog";
const systemPrompt = catalog.prompt({ system: "You are a terminal assistant." });
| Provider | Purpose |
|----------|---------|
| StateProvider | Share state across components (JSON Pointer paths). Accepts optional store prop for controlled mode. |
| ActionProvider | Handle actions dispatched via the event system |
| VisibilityProvider | Enable conditional rendering based on state |
| ValidationProvider | Form field validation |
| FocusProvider | Manage focus across interactive components |
| JSONUIProvider | Combined provider for all contexts |
Pass a StateStore to StateProvider (or JSONUIProvider) to use external state management:
import { createStateStore, type StateStore } from "@json-render/ink";
const store = createStateStore({ count: 0 });
<StateProvider store={store}>{children}</StateProvider>
store.set("/count", 1); // React re-renders automatically
When store is provided, initialState and onStateChange are ignored.
import { createRenderer } from "@json-render/ink";
import { standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";
const InkRenderer = createRenderer(catalog, {
...standardComponents,
// custom component overrides here
});
// InkRenderer includes all providers (state, visibility, actions, focus)
render(
<InkRenderer spec={spec} state={{ activeTab: "overview" }} />
);
| Export | Purpose |
|--------|---------|
| defineRegistry | Create a type-safe component registry from a catalog |
| Renderer | Render a spec using a registry |
| createRenderer | Higher-level: creates a component with built-in providers |
| JSONUIProvider | Combined provider for all contexts |
| schema | Ink flat element map schema (includes built-in state actions) |
| standardComponentDefinitions | Catalog definitions for all standard components |
| standardActionDefinitions | Catalog definitions for standard actions |
| standardComponents | Pre-built component implementations |
| useStateStore | Access state context |
| useStateValue | Get single value from state |
| useBoundProp | Two-way binding for $bindState/$bindItem expressions |
| useActions | Access actions context |
| useAction | Get a single action dispatch function |
| useOptionalValidation | Non-throwing variant of useValidation |
| useUIStream | Stream specs from an API endpoint |
| createStateStore | Create a framework-agnostic in-memory StateStore |
| StateStore | Interface for plugging in external state management |
| Components | Typed component map (catalog-aware) |
| Actions | Typed action map (catalog-aware) |
| ComponentContext | Typed component context (catalog-aware) |
| flatToTree | Convert flat element map to tree structure |
development
Create professional equity research earnings update reports (8-12 pages, 3,000-5,000 words) analyzing quarterly results for companies already under coverage. Fast-turnaround format focusing on beat/miss analysis, key metrics, updated estimates, and revised thesis. Includes 1-3 summary tables and 8-12 charts. Use when user requests "earnings update", "quarterly update", "earnings analysis", "Q1/Q2/Q3/Q4 results", or post-earnings report.
development
Updates a presentation with new numbers — quarterly refreshes, earnings updates, comp rolls, rebased market data. Use whenever the user asks to "update the deck with Q4 numbers", "refresh the comps", "roll this forward", "swap in the new earnings", "change all the $485M to $512M", or any request to swap figures across an existing deck without rebuilding it.
development
Real DCF (Discounted Cash Flow) model creation for equity valuation. Retrieves financial data from SEC filings and analyst reports, builds comprehensive cash flow projections with proper WACC calculations, performs sensitivity analysis, and outputs professional Excel models with executive summaries. Use when users need to value a company using DCF methodology, request intrinsic value analysis, or ask for detailed financial modeling with growth projections and terminal value calculations.
tools
Build professional financial services data packs from various sources including CIMs, offering memorandums, SEC filings, web search, or MCP servers. Extract, normalize, and standardize financial data into investment committee-ready Excel workbooks with consistent structure, proper formatting, and documented assumptions. Use for M&A due diligence, private equity analysis, investment committee materials, and standardizing financial reporting across portfolio companies. Do not use for simple financial calculations or working with already-completed data packs.