# @wolf-tui/vue

### Build terminal UIs with Vue 3 — flexbox layouts, styled components, keyboard input

[![Vue 3.5+](https://img.shields.io/badge/vue-%5E3.5.0-42b883)](https://vuejs.org)
[![Node](https://img.shields.io/badge/node-%3E%3D20-339933)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)

[Install](#install) · [Quick Start](#quick-start) · [Components](#components) · [Composables](#composables) · [Theming](#theming) · [CSS Styling](#css-styling)

---

## The Problem

Vue has no terminal rendering target. If you want to build CLI apps with Vue's Composition API and SFC syntax, you need a custom renderer that maps Vue's virtual DOM to terminal output.

This package provides that renderer, plus 20+ components (inputs, selects, alerts, spinners, progress bars, lists) and composables (`useInput`, `useFocus`, etc.) — all using Vue 3's Composition API.

If you've used [Ink](https://github.com/vadimdemedes/ink) for React terminal UIs, this is the Vue equivalent. It uses the same layout engine (Taffy) and shared render functions as wolf-tui's React, Angular, Solid, and Svelte adapters.

---

## Install

### Scaffold a new project (recommended)

```bash
npm create wolf-tui -- --framework vue
```

Generates a complete project with bundler config, TypeScript, and optional CSS tooling. See [create-wolf-tui](../create-wolf-tui/README.md).

### Manual setup

```bash
# Runtime dependencies
pnpm add @wolf-tui/vue vue

# Build tooling
pnpm add -D @wolf-tui/plugin @vitejs/plugin-vue vite
```

| Peer dependency | Version |
| --------------- | ------- |
| `vue`           | ^3.5.0  |

---

## Quick Start

### SFC (Single File Components)

```vue
<!-- App.vue -->
<script setup>
import { Box, Text, useInput, useApp } from '@wolf-tui/vue'
import { ref } from 'vue'

const count = ref(0)
const { exit } = useApp()

useInput((input, key) => {
	if (key.upArrow) count.value++
	if (key.downArrow) count.value = Math.max(0, count.value - 1)
	if (input === 'q') exit()
})
</script>

<template>
	<Box :style="{ flexDirection: 'column', padding: 1 }">
		<Text :style="{ color: 'green', fontWeight: 'bold' }"
			>Counter: {{ count }}</Text
		>
		<Text :style="{ color: 'gray' }">↑/↓ to change, q to quit</Text>
	</Box>
</template>
```

```ts
// index.ts
import { render } from '@wolf-tui/vue'
import App from './App.vue'

render(App)
```

> For CSS class-based styling (`class="text-green p-1"`), see [CSS Styling](#css-styling).

### Vite Configuration

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { wolfie } from '@wolf-tui/plugin/vite'

export default defineConfig({
	plugins: [
		vue({
			template: {
				compilerOptions: {
					// wolf-tui uses custom elements internally — tell Vue not to resolve them
					isCustomElement: (tag) => tag.startsWith('wolfie-'),
					hoistStatic: false,
				},
			},
		}),
		wolfie('vue'),
	],
	build: {
		lib: {
			entry: 'src/index.ts',
			formats: ['cjs'],
			fileName: 'index',
		},
		rollupOptions: {
			external: [/^vue(\/|$)/, /^@wolf-tui\//],
		},
	},
})
```

```bash
vite build && node dist/index.cjs
```

<details>
<summary><b>JSX/TSX alternative</b></summary>

```tsx
import { defineComponent, ref } from '@wolf-tui/vue'
import { Box, Text, render, useInput, useApp } from '@wolf-tui/vue'

const App = defineComponent({
	setup() {
		const count = ref(0)
		const { exit } = useApp()

		useInput((input, key) => {
			if (key.upArrow) count.value++
			if (input === 'q') exit()
		})

		return () => (
			<Box style={{ flexDirection: 'column', padding: 1 }}>
				<Text style={{ color: 'green', fontWeight: 'bold' }}>
					Counter: {count.value}
				</Text>
			</Box>
		)
	},
})

render(App)
```

</details>

---

## `render(component, options?)`

Mounts a Vue component to the terminal.

```ts
const instance = render(App, {
	stdout: process.stdout,
	stdin: process.stdin,
	maxFps: 30,
})
```

| Option                  | Type                 | Default          | Description              |
| ----------------------- | -------------------- | ---------------- | ------------------------ |
| `stdout`                | `NodeJS.WriteStream` | `process.stdout` | Output stream            |
| `stdin`                 | `NodeJS.ReadStream`  | `process.stdin`  | Input stream             |
| `stderr`                | `NodeJS.WriteStream` | `process.stderr` | Error stream             |
| `maxFps`                | `number`             | `30`             | Maximum render frequency |
| `debug`                 | `boolean`            | `false`          | Disable frame throttling |
| `isScreenReaderEnabled` | `boolean`            | env-based        | Screen reader mode       |
| `theme`                 | `ITheme`             | `{}`             | Component theming        |

---

## Components

### Layout

| Component      | Description                                 | Key features                                               |
| -------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `<Box>`        | Flexbox/Grid layout container               | All CSS-like flex props, `:style` object, `class`          |
| `<Text>`       | Styled inline text                          | Color, bold/italic/underline, wrap modes                   |
| `<Newline>`    | Empty lines                                 | `:count` prop                                              |
| `<Spacer>`     | Fills remaining flex space                  | Pushes siblings apart in flex containers                   |
| `<Static>`     | Renders items once, skips re-renders        | Append-only logs, scroll-back history                      |
| `<Transform>`  | Transforms rendered text of children        | `transform: (line, idx) => string`                         |
| `<ScrollView>` | Fixed-height viewport with clipped overflow | Built-in arrow / PageUp / PageDown / Home / End navigation |
| `<Table>`      | Box-drawing table for tabular data          | `ink-table` parity, themable borders/cells, column subset  |

<details>
<summary><b>Box & Text props</b></summary>

Both accept `style` (inline object) and `class`/`className` (CSS classes via `@wolf-tui/plugin`).

**Box style properties** (passed via `:style`):

| Property         | Type                                                                          | Description         |
| ---------------- | ----------------------------------------------------------------------------- | ------------------- |
| `flexDirection`  | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'`                      | Flex direction      |
| `flexWrap`       | `'wrap' \| 'nowrap' \| 'wrap-reverse'`                                        | Flex wrap           |
| `flexGrow`       | `number`                                                                      | Grow factor         |
| `flexShrink`     | `number`                                                                      | Shrink factor       |
| `alignItems`     | `'flex-start' \| 'center' \| 'flex-end' \| 'stretch'`                         | Cross-axis          |
| `justifyContent` | `'flex-start' \| 'center' \| 'flex-end' \| 'space-between' \| 'space-around'` | Main-axis           |
| `gap`            | `number`                                                                      | Gap between items   |
| `width`          | `number \| string`                                                            | Width               |
| `height`         | `number \| string`                                                            | Height              |
| `padding`        | `number`                                                                      | Padding (all sides) |
| `margin`         | `number`                                                                      | Margin (all sides)  |
| `borderStyle`    | `'single' \| 'double' \| 'round' \| 'classic'`                                | Border style        |
| `borderColor`    | `string`                                                                      | Border color        |
| `overflow`       | `'visible' \| 'hidden'`                                                       | Overflow behavior   |

**Text style properties** (passed via `:style`):

| Property          | Type                                     | Description      |
| ----------------- | ---------------------------------------- | ---------------- |
| `color`           | `string`                                 | Text color       |
| `backgroundColor` | `string`                                 | Background color |
| `fontWeight`      | `'bold'`                                 | Bold text        |
| `fontStyle`       | `'italic'`                               | Italic text      |
| `textDecoration`  | `'underline' \| 'line-through'`          | Decoration       |
| `inverse`         | `boolean`                                | Inverse colors   |
| `textWrap`        | `'wrap' \| 'truncate' \| 'truncate-end'` | Wrap mode        |

</details>

<details>
<summary><b>ScrollView props</b></summary>

Renders children inside a fixed-height viewport, clips overflow, and scrolls via `marginTop: -offset`. Built-in key bindings: ↑/↓ (row), PageUp/PageDown (viewport), Home/End. Adapted from [ink-scroll-view](https://github.com/ByteLandTechnology/ink-scroll-view).

| Prop                    | Type                       | Default | Description                                        |
| ----------------------- | -------------------------- | ------- | -------------------------------------------------- |
| `height`                | `number`                   | —       | Viewport height in rows (required)                 |
| `offset`                | `number`                   | —       | Controlled scroll offset — omit for internal state |
| `keyBindings`           | `boolean`                  | `true`  | Enable arrows + page + home/end                    |
| `onScroll`              | `(offset: number) => void` | —       | Fires when offset changes                          |
| `onContentHeightChange` | `(height: number) => void` | —       | Fires when measured content height changes         |

Imperative handle (via template `ref` + `defineExpose`): `scrollTo(offset)`, `scrollBy(delta)`, `scrollToTop()`, `scrollToBottom()`, `getScrollOffset()`, `getContentHeight()`, `getViewportHeight()`.

</details>

### Display

| Component         | Description                            | Key features                                                   |
| ----------------- | -------------------------------------- | -------------------------------------------------------------- |
| `<Alert>`         | Boxed alert message                    | `variant`: `success` / `error` / `warning` / `info` + title    |
| `<Badge>`         | Inline coloured label                  | `color` prop, slot = label                                     |
| `<Spinner>`       | Animated loading spinner               | 80+ `type`s (dots, line, arc, …), optional `label`             |
| `<ProgressBar>`   | Horizontal progress bar                | `:value` 0–100, custom characters, themable colors             |
| `<StatusMessage>` | One-line status with icon              | `variant`: `success` / `error` / `warning` / `info`            |
| `<ErrorOverview>` | Formatted error display                | Pretty stack trace, source frame highlight                     |
| `<Gradient>`      | Coloured text gradient                 | 13 presets or custom hex stops, per-character interpolation    |
| `<BigText>`       | ASCII-art figlet-style banner          | `cfonts` engine, multiple fonts, gradients, alignment          |
| `<Timer>`         | Count-up, countdown, or stopwatch      | Lap recording, configurable format, drift-resistant            |
| `<TreeView>`      | Hierarchical tree with expand/collapse | Single/multi-select, async lazy loading, virtual scroll        |
| `<JsonViewer>`    | Interactive JSON tree viewer           | 16 value types, syntax colouring, circular-reference detection |
| `<FilePicker>`    | Filesystem browser with filter mode    | Multi-select, symlinks, directory navigation                   |

### Input

| Component         | Description                         | Key features                                            |
| ----------------- | ----------------------------------- | ------------------------------------------------------- |
| `<TextInput>`     | Single-line text field              | `onChange` / `onSubmit`, placeholder, mask, suggestions |
| `<PasswordInput>` | Masked text input                   | Configurable mask character                             |
| `<EmailInput>`    | Email field with domain suggestions | Auto-completes top-100 email domains                    |
| `<ConfirmInput>`  | Yes / No prompt                     | y / n keys, customizable defaults                       |
| `<Select>`        | Single-selection picker             | Keyboard nav, themed indicator, `:options` array        |
| `<MultiSelect>`   | Multi-selection picker              | Toggle with space, submit with enter                    |
| `<Combobox>`      | Fuzzy-search autocomplete dropdown  | Two-pass fzf-style matching, cursor nav, autofill       |

### Lists

| Component         | Description   | Key features                   |
| ----------------- | ------------- | ------------------------------ |
| `<OrderedList>`   | Numbered list | `<OrderedListItem>` children   |
| `<UnorderedList>` | Bulleted list | `<UnorderedListItem>` children |

<details>
<summary><b>Component examples</b></summary>

```vue
<template>
	<!-- Alert (uses slot for message) -->
	<Alert variant="success" title="Deployed"> All services are running. </Alert>

	<!-- Badge (uses slot for label) -->
	<Badge color="green">NEW</Badge>

	<!-- StatusMessage (uses slot for message) -->
	<StatusMessage variant="success">Saved!</StatusMessage>

	<!-- TextInput -->
	<TextInput
		placeholder="Your name..."
		:onChange="(v) => console.log(v)"
		:onSubmit="(v) => console.log('done:', v)"
	/>

	<!-- Select -->
	<Select
		:options="[
			{ label: 'TypeScript', value: 'ts' },
			{ label: 'JavaScript', value: 'js' },
		]"
		:onChange="(v) => console.log('Picked:', v)"
	/>

	<!-- ProgressBar -->
	<ProgressBar :value="75" />

	<!-- Spinner -->
	<Spinner type="dots" label="Loading..." />

	<!-- Lists -->
	<OrderedList>
		<OrderedListItem>First</OrderedListItem>
		<OrderedListItem>Second</OrderedListItem>
	</OrderedList>

	<!-- Timer (countdown) -->
	<Timer
		variant="countdown"
		:durationMs="60000"
		format="human"
		@complete="onDone"
	/>

	<!-- TreeView -->
	<TreeView :data="treeData" selectionMode="single" @selectChange="onSelect" />

	<!-- Combobox -->
	<Combobox :options="items" placeholder="Search..." @select="onPick" />

	<!-- JsonViewer -->
	<JsonViewer :data="jsonData" :defaultExpandDepth="2" />

	<!-- FilePicker -->
	<FilePicker initialPath="." multiSelect @select="onFiles" />

	<!-- Table (ink-table parity) -->
	<Table :data="rows" :columns="['id', 'name']" :padding="1" />
	<!-- ScrollView — uncontrolled, built-in arrows/PageUp/PageDown/Home/End -->
	<ScrollView :height="8" :onScroll="(o) => console.log('offset', o)">
		<Text v-for="(it, i) in items" :key="i">{{ it }}</Text>
	</ScrollView>

	<!-- ScrollView — imperative handle via template ref -->
	<ScrollView
		ref="scrollRef"
		:height="8"
		:offset="offset"
		:onScroll="(o) => (offset = o)"
	/>
	<!-- scrollRef.value?.scrollToBottom() -->
	<!-- Gradient — by preset name (slot for text) -->
	<Gradient name="rainbow">wolf-tui in color</Gradient>

	<!-- Gradient — custom stops -->
	<Gradient :colors="['#ff3366', '#ffd700']">Hand-picked stops</Gradient>
</template>
```

</details>

---

## Composables

### `useInput(handler, options?)`

Handle keyboard input. Available inside any component rendered by `render()`.

```vue
<script setup>
import { useInput } from '@wolf-tui/vue'

useInput((input, key) => {
	if (key.upArrow) {
		/* move up */
	}
	if (key.return) {
		/* confirm */
	}
	if (input === 'q') {
		/* quit */
	}
})
</script>
```

<details>
<summary><b>Key object properties</b></summary>

| Property     | Type      | Description         |
| ------------ | --------- | ------------------- |
| `upArrow`    | `boolean` | Up arrow pressed    |
| `downArrow`  | `boolean` | Down arrow pressed  |
| `leftArrow`  | `boolean` | Left arrow pressed  |
| `rightArrow` | `boolean` | Right arrow pressed |
| `return`     | `boolean` | Enter pressed       |
| `escape`     | `boolean` | Escape pressed      |
| `ctrl`       | `boolean` | Ctrl held           |
| `shift`      | `boolean` | Shift held          |
| `meta`       | `boolean` | Meta key held       |
| `tab`        | `boolean` | Tab pressed         |
| `backspace`  | `boolean` | Backspace pressed   |
| `delete`     | `boolean` | Delete pressed      |

The `isActive` option accepts a ref, getter, or plain boolean (`MaybeRefOrGetter<boolean>`).

</details>

### `useApp()`

Access the app context — primarily for `exit()`.

```vue
<script setup>
import { useApp } from '@wolf-tui/vue'
const { exit } = useApp()
</script>
```

### `useFocus(options?)` / `useFocusManager()`

Make components focusable and control focus programmatically.

```vue
<script setup>
import { useFocus, useFocusManager } from '@wolf-tui/vue'

const { isFocused } = useFocus({ autoFocus: true })
const { focusNext, focusPrevious } = useFocusManager()
</script>
```

### Stream access

| Composable    | Returns                                     |
| ------------- | ------------------------------------------- |
| `useStdin()`  | `{ stdin, setRawMode, isRawModeSupported }` |
| `useStdout()` | `{ stdout, write }`                         |
| `useStderr()` | `{ stderr, write }`                         |

### Accessibility

| Composable                   | Returns   | Notes                                        |
| ---------------------------- | --------- | -------------------------------------------- |
| `useIsScreenReaderEnabled()` | `boolean` | Render alternative output for screen readers |

```vue
<script setup>
import { useIsScreenReaderEnabled } from '@wolf-tui/vue'

const srEnabled = useIsScreenReaderEnabled()
</script>

<template>
	<Text>{{ srEnabled ? 'Welcome, screen reader user' : 'Welcome' }}</Text>
</template>
```

---

## Theming

Customize component appearance via the `theme` option in `render()`:

```ts
import { render, extendTheme, defaultTheme } from '@wolf-tui/vue'

const theme = extendTheme(defaultTheme, {
	components: {
		Spinner: { styles: { spinner: { color: 'cyan' } } },
		Alert: { styles: { container: { borderColor: 'blue' } } },
	},
})

render(App, { theme })
```

Or provide theme via Vue's injection system:

```vue
<script setup>
import { provideTheme, extendTheme, defaultTheme } from '@wolf-tui/vue'

provideTheme(
	extendTheme(defaultTheme, {
		/* overrides */
	})
)
</script>
```

| Export                         | Description                                    |
| ------------------------------ | ---------------------------------------------- |
| `extendTheme(base, overrides)` | Deep-merge overrides into base theme           |
| `defaultTheme`                 | Base theme object                              |
| `provideTheme(theme)`          | Provide theme via Vue injection                |
| `useComponentTheme(name)`      | Read theme for a component (inside components) |

---

## CSS Styling

Three approaches, all via `@wolf-tui/plugin`:

| Method       | Usage                                       |
| ------------ | ------------------------------------------- |
| Tailwind CSS | `class="text-green p-1"` + PostCSS setup    |
| CSS Modules  | `:class="$style.box"` with `<style module>` |
| SCSS/LESS    | `class="my-class"` + preprocessor           |

All resolve to inline terminal styles at build time — no runtime CSS engine.

<details>
<summary><b>Styling examples</b></summary>

**Tailwind CSS:**

```vue
<template>
	<Box class="flex-col p-4 gap-2">
		<Text class="text-green-500 font-bold">Tailwind styled</Text>
	</Box>
</template>

<style>
@import 'tailwindcss';
</style>
```

**CSS Modules:**

```vue
<template>
	<Box :class="$style.container">
		<Text :class="$style.title">CSS Modules</Text>
	</Box>
</template>

<style module>
.container {
	flex-direction: column;
	padding: 1rem;
}
.title {
	color: green;
	font-weight: bold;
}
</style>
```

</details>

---

## TypeScript Setup

<details>
<summary><b>Template IntelliSense and CSS module types</b></summary>

For full IntelliSense in Vue templates (Volar/vue-tsc), add global component types:

```ts
// env.d.ts
/// <reference types="@wolf-tui/vue/global" />

declare module '*.vue' {
	import type { DefineComponent } from 'vue'
	const component: DefineComponent<object, object, unknown>
	export default component
}
```

For CSS module autocomplete with actual class names, install the TypeScript plugin:

```bash
pnpm add -D @wolf-tui/typescript-plugin
```

```json
// tsconfig.json
{
	"compilerOptions": {
		"plugins": [{ "name": "@wolf-tui/typescript-plugin" }]
	}
}
```

For wolfie-specific CSS property suggestions in VS Code:

```json
// .vscode/settings.json
{
	"css.customData": ["./node_modules/@wolf-tui/plugin/wolfie.css-data.json"]
}
```

</details>

---

## Vue API Re-exports

Commonly used Vue APIs are re-exported for convenience:

```ts
import {
	ref,
	reactive,
	computed,
	watch,
	watchEffect,
	onMounted,
	onUnmounted,
	provide,
	inject,
	defineComponent,
	h,
} from '@wolf-tui/vue'
```

---

## Testing

Import the testing-aware `render` from `@wolf-tui/vue/testing` to drive components headlessly. It wires up virtual `stdout`/`stdin`, registers the instance for global `cleanup()`, and re-exports `KEYS`, `delay`, `stripAnsi`, and `cleanup` from `@wolf-tui/testing-library`.

```ts
import { afterEach, test, expect } from 'vitest'
import { render, cleanup, KEYS, delay, stripAnsi } from '@wolf-tui/vue/testing'
import App from './App.vue'

afterEach(cleanup)

test('navigates the menu', async () => {
	const { stdin, lastFrame } = render(App, { columns: 80, rows: 24 })

	await stdin.write(KEYS.DOWN)
	await stdin.write(KEYS.ENTER)
	await delay(100)

	expect(stripAnsi(lastFrame() ?? '')).toContain('Selection: Option B')
})
```

Run `npm create wolf-tui -- --test` to get Vitest, `@wolf-tui/testing-library`, and a pre-wired `test/setup.ts` scaffolded automatically. See the [testing-library README](../testing-library/README.md) for the full API.

---

## Part of wolf-tui

This is the Vue adapter for [wolf-tui](../../README.md) — a framework-agnostic terminal UI library. The same layout engine (Taffy/flexbox) and component render functions power adapters for React, Angular, Solid, and Svelte.

<details>
<summary><b>Bundler examples</b></summary>

The `examples/` directory has working setups for each bundler:

| Bundler | Example                 |
| ------- | ----------------------- |
| Vite    | `examples/vue_vite/`    |
| esbuild | `examples/vue_esbuild/` |
| webpack | `examples/vue_webpack/` |

</details>

## License

MIT
