| kind | schema |
|---|---|
| name | _schema |
| description | Meta-spec that defines the format for all TUIkit component and token specs. Agents read this before generating new specs or compiling specs to code. |
| version | 2 |
This document defines the spec format used to describe TUIkit tokens and components in a language-agnostic way. Specs are the source of truth for cross-language code generation.
All specs use RFC 2119 keywords to distinguish normative requirements from informative guidance:
- MUST / MUST NOT: Absolute requirement or prohibition. Agents MUST generate code that enforces this.
- SHOULD / SHOULD NOT: Strong recommendation. Agents SHOULD generate this unless the target framework makes it impractical.
- MAY: Optional behavior. Agents MAY omit or make configurable.
All Behavior, Visual rules, and Edge cases sections MUST use conformance keywords for any statement that affects rendered output or interaction logic.
specs/
docs/schema.md ← this file (meta-spec)
README.md ← getting started guide
compile.ts ← compiler CLI (status/prompt/lock/clean)
lint.ts ← linter CLI (validate specs against schema)
targets/
go.md ← Go + Bubbletea + Lipgloss
bun.md ← Bun + Ink + React
rust.md ← Rust + Ratatui + Crossterm
{target}.lock.json ← generated lock file (tracks compiled state)
tokens/
colors.md ← semantic color tokens
icons.md ← icon glyphs and semantic aliases
breakpoints.md ← responsive width thresholds
components/
{Name}/
{Name}.md ← component spec
{Name}.test.md ← component test spec
{Name}.preview.md ← component preview spec
dist/ ← compiled output (gitignored)
{target}/ ← generated code per target
_compile-prompt.md ← the prompt fed to the agent
... ← generated source files
The compiler detects spec changes via content hashing and generates self-contained prompts for LLM agents.
# Show dirty/clean status for all targets
bun scripts/compile.ts status
# Show status for a specific target
bun scripts/compile.ts status --target go
# Generate compilation prompt for all dirty specs
bun scripts/compile.ts prompt --target go
# Generate prompt for a single component
bun scripts/compile.ts prompt --target go --component HintBar
# Lock spec hashes after successful compilation
bun scripts/compile.ts lock --target go
# Remove lock file and generated prompts
bun scripts/compile.ts clean --target goLock files track which spec versions have been compiled. A spec is "dirty" when its content hash differs from the locked hash, or when the schema hash changes (invalidating all entries).
{
"target": "go",
"schemaHash": "ea8e9c21ed0044cc",
"updatedAt": "2026-03-28T00:00:00.000Z",
"entries": {
"components/HintBar": {
"version": "1",
"specHash": "a1b2c3d4e5f60718",
"testHash": "1234abcd5678ef90",
"lockedAt": "2026-03-28T00:00:00.000Z"
}
}
}- Edit specs (component, token, or test files)
- Run
statusto see what changed - Run
prompt --target <name>to generate a compilation prompt - Feed the prompt to an LLM agent (e.g. Copilot CLI)
- Verify generated code passes tests
- Run
lock --target <name>to snapshot current hashes
Token specs define shared design values consumed by all components.
Every token MUST be exposed at runtime through a token provider — the runtime layer between static token definitions and live component rendering. A token provider is a function, hook, method, or trait that resolves token values from the current environment (terminal width, color mode, theme).
Components MUST consume tokens through providers, never as hardcoded values. Providers MUST recompute when their input context changes (e.g., terminal resize triggers breakpoint recalculation, theme change triggers color update).
The provider pattern is idiomatic to each target:
- React/Ink: Hooks (e.g.,
useColors(),useBreakpoint()) that trigger re-renders when the environment changes. - Go/Bubbletea: Methods on the model or a context struct, updated
via messages (e.g.,
WindowSizeMsg). - Rust/Ratatui: Methods on a state struct or trait implementations that read from shared application state.
kind: token # always "token"
name: string # token group name (e.g., "colors", "icons", "breakpoints")
description: string # what this token group provides
version: number # spec version (increment on breaking changes)Token specs define their data directly in frontmatter. The format varies by token type:
- colors:
tokens:map with ramp coordinates and fallbacks per color mode - icons:
groups:map with glyph definitions and semantic aliases - breakpoints:
values:map with width thresholds
The markdown body contains:
- Design principles (why these tokens exist)
- Implementation guide (how to build them in a target language)
kind: component # always "component"
name: string # PascalCase component name
description: string # one-line summary
version: number # spec version
category: string # one of: input, display, navigation, layout, feedbackDeclares which tokens this component uses. Agents use this to generate correct imports and ensure token availability.
tokens:
colors: [textPrimary, textSecondary, selected, ...]
icons: [iconPrompt, iconSuccess, ...]Typed prop definitions. Every prop must declare type, required, and description.
props:
propName:
type: string | number | boolean | array<T> | record<K,V> | callback(args) | T
required: boolean
default: value # only if required is false
description: stringSupported types:
- Primitives:
string,number,boolean - Collections:
array<T>,record<string, T> - Callbacks:
callback(arg1: type, arg2: type) → returnType - Unions:
string | false | null - Generic:
T(component is generic over T) - References:
SelectItem<T>(references another type defined in the spec)
Define supporting types used by props.
types:
SelectItem:
generic: T
fields:
label: { type: string, required: true, description: "Display text" }
value: { type: T, required: true, description: "Backing value" }
current: { type: boolean, required: false, description: "Marks as active choice" }Defines a state machine for interactive components. Stateless components omit this.
states:
initial: idle
definitions:
idle:
description: Component mounted, waiting for focus
transitions:
focus: focused
focused:
description: Component has keyboard focus
transitions:
select: selected
escape: dismissed
selected:
description: User confirmed a choice
terminal: true
dismissed:
description: User cancelled
terminal: trueRules:
initialis the entry stateterminal: truestates emit a callback and end interaction- Transitions are
trigger: targetStatepairs - Triggers can be keyboard keys, programmatic events, or timers
Maps keyboard input to actions. Actions reference state transitions or callbacks.
keyboard:
"↑": { action: "move selection up", wrap: true }
"↓": { action: "move selection down", wrap: true }
k: { action: "move selection up", note: "vim binding" }
j: { action: "move selection down", note: "vim binding" }
enter: { action: "confirm selection → fires onSelect" }
escape: { action: "cancel → fires onEscape" }
ctrl+g: { action: "cancel (alternative)", same_as: escape }
"1-9": { action: "select item by number (1-indexed)" }Defines responsive behavior at different terminal widths.
breakpoints:
compact: # < 80 columns
description: what changes at this breakpoint
narrow: # 80-119 columns
description: what changes at this breakpoint
wide: # ≥ 120 columns
description: what changes at this breakpointScreen reader and assistive technology behavior. Uses ARIA terminology for formal role/state/property definitions.
accessibility:
role: string # ARIA role (e.g., "listbox", "textbox", "dialog", "status")
properties: # static ARIA attributes set once on mount
aria-label: string # MUST be descriptive of the component purpose
aria-describedby: string # MAY reference hint text or description
states: # dynamic ARIA states that change during interaction
aria-selected: string # description of when true/false
aria-expanded: string # description of when true/false
announce:
on_mount: string # screen reader announcement on first render
on_change: string # announcement when state changes
screen_reader_adaptations:
- when: string # condition (e.g., "screen reader detected")
change: string # what changes in renderingRules:
- Every interactive component MUST define
roleandannounce.on_mount - Components with selection MUST define
states.aria-selected screen_reader_adaptationsMUST describe visual-to-text replacements (e.g., replacing glyphs with text suffixes)
Animation definitions with accessibility controls.
animation:
name:
frames: [frame1, frame2, ...] # or reference to icon spinner sequence
interval_ms: number
disable_when: [screen_reader, reduced_motion]Named values that are part of the component contract (not configuration). Constants differ from props in that they are fixed, not user-provided.
constants:
MIN_DIALOG_WIDTH:
type: number
value: 30
description: "Minimum character width for dialog rendering"Describes alternate forms of the component that share most behavior but differ in specific props or rendering. Variants MUST reference the base component and list only the differences.
variants:
UncontrolledInput:
description: "Input that manages its own value state internally"
props_override:
value: { required: false }
onChange: { required: false }
props_added:
defaultValue: { type: string, required: false, description: "Initial value" }Input sanitization and output safety rules. Components that accept URLs, user-provided text, or render external content MUST define this section.
security:
sanitization:
- input: url
rule: "MUST strip ESC (0x1B), BEL (0x07), and ST (0x9C) characters"
reason: "Prevent terminal escape injection via malicious URLs"Every component MUST declare its dependencies on tokens and other components. This enables the compiler to build dependency graphs and detect breaking changes.
dependencies:
tokens:
- name: selected
kind: color
usage: "Highlight indicator and text color"
required: true
- name: iconPrompt
kind: icon
usage: "Selection indicator glyph"
required: true
components:
- name: HintBar
usage: "Keyboard navigation hints in footer"
required: false
dependents: # other components that use this one
- SelectAutocomplete
- FilterMenuRules:
required: truetokens MUST be available at render time; absence is an errorrequired: falsetokens MAY be absent; component MUST render without themdependentsis informational; used by the compiler for cascade invalidation
Declares how this component composes with other components.
composition:
children:
- component: HintBar
slot: footer
default_props: { hints: { ... } }
optional: true # can be hidden via prop
slots:
footer: { description: "Bottom area for hints or actions" }The markdown body follows a consistent structure:
One paragraph describing the component's purpose.
Bullet list of styling rules. MUST reference token names, MUST NOT use raw colors. MUST use RFC 2119 keywords.
## Visual rules
- Selected item text MUST use the `selected` color token
- Selection indicator MUST use `iconPrompt`
- Current item MUST show `iconSuccess` in `statusSuccess` color
- Unselected items SHOULD use `textOnBackgroundSecondary`Shows expected output with annotations.
## Rendering example
Given items: ["Alpha", "Beta (current)", "Gamma"]
```
❯ 1. Alpha
2. Beta ✓
3. Gamma
↑↓ to navigate · Enter to select · Esc to cancel
```Describes interaction patterns. For keyboard-driven interactions, MUST use numbered algorithmic steps rather than prose:
### Escape key processing
When the user presses Escape while in the `focused` state:
1. Let item be escapeItem if provided and not null, else undefined
2. If item is defined:
a. Call onSelect(item)
b. Transition to `dismissed` state
c. Terminate processing
3. Let callback be onEscape if provided, else undefined
4. If callback is defined:
a. Call callback()
b. Transition to `dismissed` state
c. Terminate processing
5. Otherwise, remain in `focused` state and discard the keystrokeSummary table of tokens and components consumed. MUST match the
dependencies frontmatter section.
## Dependencies
| Dependency | Kind | Usage | Required |
| --------------- | --------- | ------------------- | -------- |
| `selected` | color | Highlight text | Yes |
| `statusSuccess` | color | Current item glyph | No |
| `iconPrompt` | icon | Selection indicator | Yes |
| `HintBar` | component | Footer hints | No |Describes component variants (e.g., SelectWithTextInput).
Documents boundary behavior.
kind: test
component: string # must match a component spec name
version: numberEach test is an H2 heading followed by fenced code blocks:
# Inside ```props block
items: ["Alpha", "Beta"]
onSelect: callbackThe expect block is normative. The rendered character grid is the source of truth for output validation. No alternative rendering is valid for the given props.
# Inside ```expect block — exact terminal output (normative)
❯ 1. Alpha
2. Beta
The expect block matches plain text only — no ANSI codes. The test harness strips ANSI before comparison.
# Inside ```input block
↓ # single key
↓ ↓ ↓ # sequence (space-separated)
"hello" # text input (quoted)Input blocks are applied sequentially before the next expect block.
# Inside ```style block
- selector: key("Esc") # targets a rendered text segment
bold: true
color: textPrimary # references a semantic token name
- selector: separator
color: textSecondarySelectors:
key("text")— matches a key labellabel("text")— matches an action labelitem(index)— matches a list item by 0-based indexindicator(index)— matches a selection indicatorseparator— matches separatorscomponent("Name")— matches a child component
# Inside ```state block
before: idle
trigger: focus
after: focused# Inside ```accessibility block
announce: "Select: 3 items. Alpha, Beta, Gamma. Use arrow keys to navigate."Tests within a single H2 section run top to bottom. Blocks are:
props— set up componentexpect— assert initial renderinput— simulate interactionexpect— assert updated render- (repeat input → expect as needed)
This enables multi-step interaction tests:
## navigates down then selects
`props
items: ["A", "B", "C"]
`
`expect
❯ 1. A
2. B
3. C
`
`input
↓
`
```expect
1. A
❯ 2. B
2. C
```
`input
enter
`
`state
after: selected
selected_value: "B"
`Preview specs define how a component is showcased in the demo app. Each file lists named variants with their props — the demo app renders all variants for the selected component as fully interactive, live instances.
The props block defines initial props for the variant. It does NOT mean
"render a static snapshot." Every variant MUST be a real, running component
instance that responds to keyboard input, updates state, and re-renders in
real time.
kind: preview
component: string # must match a component spec name (or "colors", "icons", "breakpoints" for tokens)
version: numberEach variant is an H2 heading followed by a props fenced code block:
## Default hints
`props
hints:
enter: "select"
esc: "cancel"
up-down: "navigate"
`
## Custom separator
`props
hints:
a: "one"
b: "two"
c: "three"
separator: " | "
`Rules:
- Each
## headingnames the variant (displayed as a label in the demo) - Each
propsblock contains YAML initial props passed to the component - Every variant MUST be a live, interactive instance — not a static render
- Variants render top to bottom in the main preview panel
- For token previews, the
propsblock contains display configuration (e.g., which token groups to show) - See
components/previews.mdfor full demo app architecture (sidebar + main panel layout, focus model, keyboard handling) - Preview specs MUST NOT duplicate test logic — they showcase visual surface area, not assert correctness
Target specs define how component/token specs compile to a specific language + framework.
kind: target
name: string # short identifier (e.g., "go", "bun", "rust")
language: string # programming language
runtime: string # runtime + minimum version
framework:
name: string # TUI framework name
version: string # minimum version
paradigm: string # architectural pattern- Architecture pattern — how the framework structures components
- Type mapping — table mapping spec types → language types
- Callback translation — how spec callbacks become idiomatic (messages, actions, props)
- State machine translation — how spec states map to the framework pattern
- Token access — how components consume color/icon/breakpoint tokens
- Styling — how to apply semantic colors and bold/italic to rendered text
- Composition — how parent components embed children
- Test pattern — how
.test.mdblocks become runnable tests - Key mapping — table mapping spec keyboard names to framework key events
- Dependencies — required packages/modules
Every target must include a demo CLI tool that renders all generated components interactively. This is the primary visual verification mechanism.
demo:
entry: path/to/demo entrypoint
run_command: "command to start the demo"
description: >
Interactive preview of all components. Cycles through each component
with live keyboard interaction.The demo must:
- Render each component in isolation so visual output can be inspected
- Support keyboard interaction (navigate, select, escape)
- Use the generated token system (colors, icons) — not hardcoded values
- Be runnable with a single command from the target directory
Each spec has a version field. When a spec changes:
- Bump
version - The lockfile (per target language) records
{ spec_hash, generated_file_hashes } - Only specs with changed hashes trigger regeneration
- The agent receives: current generated code + spec diff → produces code patch
- Tests run against patched code
- If green, lockfile updates; if red, agent iterates
| Thing | Convention | Example |
|---|---|---|
| Spec files | PascalCase.md | Select.md, HintBar.md |
| Test files | PascalCase.test.md | Select.test.md |
| Token files | lowercase.md | colors.md, icons.md |
| Props | camelCase | onSelect, hideHints |
| Tokens | camelCase | textPrimary, iconPrompt |
| States | lowercase | idle, focused, selected |
| Categories | lowercase | input, display, navigation |