@valyrianjs/terminal 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +105 -55
  2. package/dist/ansi.d.ts +20 -4
  3. package/dist/ansi.d.ts.map +1 -1
  4. package/dist/ansi.js +171 -47
  5. package/dist/ansi.js.map +1 -1
  6. package/dist/editor-state.d.ts +22 -0
  7. package/dist/editor-state.d.ts.map +1 -0
  8. package/dist/editor-state.js +110 -0
  9. package/dist/editor-state.js.map +1 -0
  10. package/dist/events.d.ts +1 -4
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js +15 -38
  13. package/dist/events.js.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/keymap.d.ts +7 -0
  19. package/dist/keymap.d.ts.map +1 -0
  20. package/dist/keymap.js +133 -0
  21. package/dist/keymap.js.map +1 -0
  22. package/dist/layout.d.ts +10 -1
  23. package/dist/layout.d.ts.map +1 -1
  24. package/dist/layout.js +97 -7
  25. package/dist/layout.js.map +1 -1
  26. package/dist/mouse.d.ts +1 -0
  27. package/dist/mouse.d.ts.map +1 -1
  28. package/dist/mouse.js +24 -1
  29. package/dist/mouse.js.map +1 -1
  30. package/dist/output-writer.d.ts +9 -0
  31. package/dist/output-writer.d.ts.map +1 -0
  32. package/dist/output-writer.js +79 -0
  33. package/dist/output-writer.js.map +1 -0
  34. package/dist/paste.d.ts +7 -0
  35. package/dist/paste.d.ts.map +1 -0
  36. package/dist/paste.js +18 -0
  37. package/dist/paste.js.map +1 -0
  38. package/dist/primitives.d.ts +15 -3
  39. package/dist/primitives.d.ts.map +1 -1
  40. package/dist/primitives.js +9 -1
  41. package/dist/primitives.js.map +1 -1
  42. package/dist/render.d.ts +9 -4
  43. package/dist/render.d.ts.map +1 -1
  44. package/dist/render.js +923 -68
  45. package/dist/render.js.map +1 -1
  46. package/dist/runtime.d.ts +29 -0
  47. package/dist/runtime.d.ts.map +1 -0
  48. package/dist/runtime.js +209 -0
  49. package/dist/runtime.js.map +1 -0
  50. package/dist/scheduler.d.ts +8 -0
  51. package/dist/scheduler.d.ts.map +1 -0
  52. package/dist/scheduler.js +24 -0
  53. package/dist/scheduler.js.map +1 -0
  54. package/dist/session.d.ts.map +1 -1
  55. package/dist/session.js +858 -199
  56. package/dist/session.js.map +1 -1
  57. package/dist/stream-log.d.ts +40 -0
  58. package/dist/stream-log.d.ts.map +1 -0
  59. package/dist/stream-log.js +73 -0
  60. package/dist/stream-log.js.map +1 -0
  61. package/dist/text.d.ts +3 -0
  62. package/dist/text.d.ts.map +1 -0
  63. package/dist/text.js +19 -0
  64. package/dist/text.js.map +1 -0
  65. package/dist/theme.d.ts +7 -0
  66. package/dist/theme.d.ts.map +1 -0
  67. package/dist/theme.js +254 -0
  68. package/dist/theme.js.map +1 -0
  69. package/dist/tree.d.ts +2 -0
  70. package/dist/tree.d.ts.map +1 -1
  71. package/dist/tree.js +42 -1
  72. package/dist/tree.js.map +1 -1
  73. package/dist/types.d.ts +203 -24
  74. package/dist/types.d.ts.map +1 -1
  75. package/docs/api-reference.md +313 -142
  76. package/docs/assets/quick-note.svg +13 -0
  77. package/docs/cookbook.md +296 -201
  78. package/docs/core-concepts.md +143 -55
  79. package/docs/getting-started.md +209 -90
  80. package/docs/interaction-model.md +98 -54
  81. package/docs/primitive-gallery.md +370 -0
  82. package/docs/session-runtime.md +131 -362
  83. package/docs/valyrian-modules.md +3196 -0
  84. package/llms-full.txt +5377 -0
  85. package/package.json +21 -8
  86. package/src/ansi.ts +269 -0
  87. package/src/clipboard.ts +76 -0
  88. package/src/editor-state.ts +162 -0
  89. package/src/events.ts +163 -0
  90. package/src/index.ts +95 -0
  91. package/src/keymap.ts +151 -0
  92. package/src/layout.ts +282 -0
  93. package/src/mouse.ts +68 -0
  94. package/src/output-writer.ts +93 -0
  95. package/src/paste.ts +23 -0
  96. package/src/primitives.ts +55 -0
  97. package/src/render.ts +1204 -0
  98. package/src/runtime.ts +267 -0
  99. package/src/scheduler.ts +33 -0
  100. package/src/session.ts +1408 -0
  101. package/src/stream-log.ts +96 -0
  102. package/src/text.ts +20 -0
  103. package/src/theme.ts +263 -0
  104. package/src/tree.ts +169 -0
  105. package/src/types.ts +541 -0
  106. package/tsconfig.json +4 -7
  107. package/docs/local-demo.md +0 -28
@@ -1,93 +1,181 @@
1
1
  # Core Concepts
2
2
 
3
- This guide explains the package mental model before going into runtime details or the API reference.
3
+ This document explains the mental model for ValyrianJS Terminal. Read it after [Getting Started](./getting-started.md), then use the Cookbook, Primitive Gallery, and interaction guides before opening the API Reference as a lookup document.
4
4
 
5
- ## 1. Terminal-First Primitives
5
+ ## Primitives
6
6
 
7
- `valyrianjs-terminal` does not render HTML or browser components. Its primitives generate their own terminal nodes, which are then converted into plain text or an ANSI frame.
7
+ ValyrianJS Terminal renders terminal nodes, not HTML. The public primitives are grouped by role:
8
8
 
9
- The core pieces are grouped like this:
9
+ - layout: `Screen`, `Box`, `View`, `Pane`, `Split`, `Fixed`, `Overlay`, `FocusScope`, `Table`, `Row`, `Td`
10
+ - content: `Text`, `LogView`
11
+ - interaction: `Input`, `Editor`, `Button`, `List`, `ScrollView`
10
12
 
11
- - layout: `Screen`, `Box`, `View`, `Text`, `Table`, `Row`, `Td`
12
- - interaction: `Input`, `Button`, `List`, `ScrollView`
13
+ `Screen` is the usual root. `Box`, `View`, `Pane`, and `Split` compose rows and columns. See [Primitive Gallery](./primitive-gallery.md) for a practical map of every primitive and complete mini apps such as [`examples/docs/primitive-layout-shell.tsx`](../examples/docs/primitive-layout-shell.tsx), which runs with `bun examples/docs/primitive-layout-shell.tsx` and exits with `Ctrl+C`. `Fixed` reserves top, bottom, left, or right regions when it is a direct child of `Screen` or `Pane`. `Overlay` draws a clipped region over the current frame.
13
14
 
14
- Think of it as a text UI DSL, not as a visual adaptation of the DOM.
15
+ `Screen`, `Box`, `View`, `Pane`, and `Split` use block-like layout defaults inside a render or session context. `Box`, `View`, and `Pane` take available width by default and keep content height by default. `Split` takes the available render area by default because it needs both axes to divide cells. Explicit numeric `width` and `height` always win. Use `fill` when a block surface should consume the full available height instead of stacking at content height.
15
16
 
16
- ## 2. Two Working Modes
17
+ ## Static render vs mounted session
17
18
 
18
- There are two main entrypoints:
19
+ There are two main entry points:
19
20
 
20
- - `renderTerminal()` to generate plain text from a terminal tree
21
- - `mountTerminal()` to create an interactive session with focus, keyboard, mouse, and optional writes to streams
21
+ - `renderTerminal()` returns plain text for a terminal tree.
22
+ - `mountTerminal()` creates a `TerminalSession` with focus, dispatch, output, lifecycle, and optional stream integration.
22
23
 
23
- Simple rule:
24
+ Use `renderTerminal()` for static output, examples, and snapshots. Use `mountTerminal()` when a UI needs interaction or repeated renders.
24
25
 
25
- - if you only want to inspect content, use `renderTerminal()`
26
- - if you need interaction or a live runtime, use `mountTerminal()`
26
+ Related example: [`examples/docs/hello.tsx`](../examples/docs/hello.tsx). Run it with `bun examples/docs/hello.tsx`, press `Enter` for details, and quit with `Ctrl+C`.
27
27
 
28
- ## 3. State Lives Outside the Library
28
+ ```tsx
29
+ import { Screen, Text, renderTerminal } from "@valyrianjs/terminal";
29
30
 
30
- The library renders whatever your UI function returns with the current state. It does not manage an application store for you.
31
+ const output = renderTerminal(
32
+ <Screen title="Static">
33
+ <Text>Hello terminal</Text>
34
+ </Screen>
35
+ );
31
36
 
32
- That means:
37
+ console.log(output);
38
+ ```
33
39
 
34
- - your handlers mutate external state
35
- - `mountTerminal()` re-evaluates the render function
36
- - `session.update()` is only needed when you mutated state outside a handler that already triggers a rerender
40
+ ## State lives outside the renderer
37
41
 
38
- ## 4. `id` Is the Key to Interaction
42
+ Keep application state in your app layer. The renderer turns the tree returned by your function into terminal output using the state your app provides.
39
43
 
40
- Interactive nodes can render without an `id`, but you lose much of the programmatic control.
44
+ Related example: [`examples/docs/component-composition.tsx`](../examples/docs/component-composition.tsx). Run it with `bun examples/docs/component-composition.tsx`, switch cards with `N/P`, and quit with `Ctrl+C`.
41
45
 
42
- You need a stable `id` if you want to:
46
+ ```tsx
47
+ import { Button, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
43
48
 
44
- - focus with `session.focus(id)`
45
- - activate with `session.click(id)`
46
- - participate in coordinate-based hitboxes
47
- - get consistent focus and navigation flows
49
+ const state = { count: 0 };
48
50
 
49
- Practical recommendation: give an `id` to every `Input`, `Button`, `List`, and `ScrollView` that will live beyond a trivial test.
51
+ function App() {
52
+ return (
53
+ <Screen title="Counter">
54
+ <Text>Count: {state.count}</Text>
55
+ <Button id="increment" onpress={() => {
56
+ state.count += 1;
57
+ }}>
58
+ Increment
59
+ </Button>
60
+ </Screen>
61
+ );
62
+ }
50
63
 
51
- ## 5. Focus and Hitboxes
64
+ const session = mountTerminal(<App />);
52
65
 
53
- The session computes hitboxes from the current rendered frame. This enables two kinds of interaction:
66
+ session.focus("increment");
67
+ session.dispatchKey("ENTER");
54
68
 
55
- - by identifier: `focus(id)`, `click(id)`
56
- - by coordinates: `focusAt(x, y)`, `clickAt(x, y)`
69
+ console.log(session.output());
70
+ session.destroy();
71
+ ```
57
72
 
58
- Sequential focus also comes from the current tree:
73
+ For larger UIs, Valyrian state modules can hold shared state outside the terminal tree. `createPulseStore` is useful when named transitions make state changes clearer.
59
74
 
60
- - `focusNext()`
61
- - `focusPrev()`
62
- - `dispatchKey("TAB")`
63
- - `dispatchKey("SHIFT_TAB")`
75
+ Related example: [`examples/docs/employees-list.tsx`](../examples/docs/employees-list.tsx). Run it with `bun examples/docs/employees-list.tsx`, choose a person with `J/K` or arrows, and quit with `Ctrl+C`.
64
76
 
65
- If the tree changes, focus order and geometry change as well.
77
+ ```tsx
78
+ import { createPulseStore } from "valyrian.js/pulses";
79
+ import { Button, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
66
80
 
67
- ## 6. Plain Output vs ANSI Output
81
+ const counter = createPulseStore(
82
+ { count: 0 },
83
+ {
84
+ increment(state) {
85
+ state.count += 1;
86
+ }
87
+ }
88
+ );
68
89
 
69
- The package works with two useful representations:
90
+ function App() {
91
+ return (
92
+ <Screen title="Pulse store">
93
+ <Text>Count: {counter.state.count}</Text>
94
+ <Button id="increment" onpress={() => counter.increment()}>
95
+ Increment
96
+ </Button>
97
+ </Screen>
98
+ );
99
+ }
70
100
 
71
- - plain output: returned by `renderTerminal()` and `session.output()`
72
- - ANSI output: returned by `session.ansiOutput()` and, if you enable `ansi: true`, also written to `stdout`
101
+ const session = mountTerminal(<App />);
73
102
 
74
- Use plain output for snapshots and tests. Use ANSI when you need cursor control, visual spans, focus, or integration with a real terminal.
103
+ session.focus("increment");
104
+ session.dispatchKey("ENTER");
75
105
 
76
- ## 7. Streams and Cleanup
106
+ console.log(session.output());
107
+ session.destroy();
108
+ ```
77
109
 
78
- `mountTerminal()` can work without streams, but if you connect `stdin` and `stdout` the session enters real runtime mode.
110
+ If components read Valyrian reactive state, the terminal refreshes when that state changes. Call `session.update()` for non-reactive state or for an explicit manual refresh.
79
111
 
80
- When you mount with `stdin`:
112
+ ## Valyrian.js modules as app-layer state and workflow
81
113
 
82
- - the session listens for `data`
83
- - it tries to enable raw mode if the stream supports it
84
- - it tries to restore it when you call `destroy()`
114
+ Valyrian.js modules in terminal apps stay in the app layer. The terminal adapter renders terminal nodes, handles focus, dispatches input, and exposes session output. Your app still owns routing choices, command design, data loading, persistence, and state transitions.
85
115
 
86
- That is why `destroy()` is not optional when you connect real streams.
116
+ Use `valyrian.js/pulses` and `valyrian.js/flux-store` for shared state, `valyrian.js/forms` for canonical input state, `valyrian.js/request` and `valyrian.js/query` for API and cached reads, `valyrian.js/tasks` for async actions, `valyrian.js/translate` and `valyrian.js/money` for user-facing formatting, and `valyrian.js/native-store` plus `valyrian.js/utils` for local settings and helper logic. When async work changes Valyrian reactive state read by components, the terminal refreshes automatically.
87
117
 
88
- ## 8. Where to Go Next
118
+ Read the dedicated guide at [`docs/valyrian-modules.md`](./valyrian-modules.md) for terminal-specific module patterns. Related complete demos: **Operations Workbench** ([`examples/docs/module-state-workbench.tsx`](../examples/docs/module-state-workbench.tsx)), **API Operations Dashboard** ([`examples/docs/module-api-dashboard.tsx`](../examples/docs/module-api-dashboard.tsx)), and **Billing/Settings Wizard** ([`examples/docs/module-form-workflow.tsx`](../examples/docs/module-form-workflow.tsx)). Run them with `bun examples/docs/module-state-workbench.tsx`, `bun examples/docs/module-api-dashboard.tsx`, or `bun examples/docs/module-form-workflow.tsx`, try the keys shown in each footer, and quit with `Ctrl+C`.
89
119
 
90
- - `docs/interaction-model.md` explains what each interactive primitive does
91
- - `docs/session-runtime.md` explains lifecycle, streams, clipboard, coordinates, and mouse support
92
- - `docs/cookbook.md` shows short recipes for common tasks
93
- - `docs/api-reference.md` works as a point reference
120
+ ## Stable ids and focus
121
+
122
+ Interactive nodes can render without an `id`, but stable ids are required for most programmatic interaction.
123
+
124
+ Use stable ids when you need:
125
+
126
+ - `session.focus(id)`
127
+ - `session.click(id)`
128
+ - `focusAt(x, y)` or `clickAt(x, y)` to resolve a hitbox consistently
129
+ - keyboard traversal with predictable focus order
130
+
131
+ Practical rule: give every long-lived `Input`, `Editor`, `Button`, `List`, and `ScrollView` an `id`.
132
+
133
+ Focus belongs to the `TerminalSession`; app state remains yours. `FocusScope` can bound sequential traversal inside a local group, and `Overlay trapFocus` can keep traversal inside an overlay while it is present.
134
+
135
+ ## Styling and visual states
136
+
137
+ Use `theme.styles` for named recipes with hex colors, border, and padding. The default theme already gives `Button`, `Input`, and `List` app-like base, focus, selection, current, hover, loading, and disabled styles, so `<Button><Text>Ok</Text></Button>` renders as a styled control without per-instance chrome. When a primitive has a matching `<element>.base` recipe, the renderer applies it automatically before instance styles. Components still use `style` for their base recipe, `styles` to map visual states to recipes, and `state` when the app owns a visual status. Raw span serialization tokens belong to low-level renderer integrations; normal styling uses semantic styles and hex colors.
138
+
139
+ Related example: [`examples/docs/style-system.tsx`](../examples/docs/style-system.tsx). Run it with `bun examples/docs/style-system.tsx`, try `Tab`, `Enter`, and `W`, and quit with `Ctrl+C`.
140
+
141
+ Common states include `focus`, `hover`, `disabled`, and `loading`. Controls can use `pressed`, `checked`, `unchecked`, and `indeterminate`. Navigation can use `selected`, `current`, `expanded`, and `collapsed`. Entry fields can use `invalid`, `valid`, `readonly`, `placeholder`, `selection`, `editing`, and `submitted`. Content can use `empty`, `error`, `warning`, `success`, and `muted`. Pointer-style composition can use `dragging`, `dropTarget`, and `capturing`.
142
+
143
+ Use `style={{ background: "#111111" }}` or a theme recipe to fill a surface. `margin` is not included; compose spacing with layout, padding, and explicit text when needed.
144
+
145
+ ## Plain text vs ANSI output
146
+
147
+ The package exposes two output representations:
148
+
149
+ - plain text: `renderTerminal()` and `session.output()`
150
+ - ANSI output: `session.ansiOutput()` and app-runtime `stdout` writes
151
+
152
+ Plain text belongs to `renderTerminal()`, `session.output()`, snapshots, and scripted checks. ANSI output is for runtime integration when a terminal needs cursor movement or style spans.
153
+
154
+ Keep ordinary rendered content as text. Do not build normal app behavior by placing escape sequences in `Text`, input values, list rows, or log entries. Use semantic spans, themes, and the documented runtime output paths instead.
155
+
156
+ Style borders are opt-in through style recipes. Plain output remains readable, and ANSI output applies style spans when supported.
157
+
158
+ ## Cleanup and lifecycle
159
+
160
+ `mountTerminal()` can run without streams, which is useful for examples and scripted interaction. When you connect real streams, cleanup matters.
161
+
162
+ The session lifecycle is:
163
+
164
+ 1. mount with `mountTerminal()`
165
+ 2. read `output()` or `ansiOutput()`
166
+ 3. dispatch input or mutate state
167
+ 4. call `update()` when needed
168
+ 5. call `destroy()` when finished
169
+
170
+ When `stdin` is connected, the session may attach listeners, enable raw mode when supported, and resume the stream. `destroy()` removes listeners and restores terminal state where the connected streams support it.
171
+
172
+ Connected streams follow terminal and runtime capabilities. Use [Session Runtime](./session-runtime.md) when integrating with `stdin`, `stdout`, resize events, clipboard adapters, or mouse input.
173
+
174
+ ## Where to go next
175
+
176
+ - [Cookbook](./cookbook.md) for practical recipes.
177
+ - [Primitive Gallery](./primitive-gallery.md) for choosing and composing public terminal primitives.
178
+ - [Interaction Model](./interaction-model.md) for focus, keys, mouse, and event payloads.
179
+ - [Session Runtime](./session-runtime.md) for common usage and advanced host integration.
180
+ - [Valyrian Modules](./valyrian-modules.md) for terminal app patterns with Valyrian.js modules.
181
+ - [API Reference](./api-reference.md) for props and session methods.
@@ -1,148 +1,267 @@
1
1
  # Getting Started
2
2
 
3
- This guide covers the minimum happy path to get started with `valyrianjs-terminal`.
3
+ This guide creates a first functional terminal UI. You will configure JSX, create `main.tsx`, render a static frame, then mount an interactive session.
4
4
 
5
- ## 1. Install the Packages
5
+ ## What you will build
6
+
7
+ You will build a small note UI with:
8
+
9
+ - a static render path for snapshots or one-off output
10
+ - an interactive input path for keyboard-driven sessions
11
+ - a simple bridge from local state to Valyrian state modules when the UI grows
12
+
13
+ ## Install
6
14
 
7
15
  ```bash
8
- npm install valyrianjs-terminal valyrian.js
16
+ npm install @valyrianjs/terminal valyrian.js
9
17
  ```
10
18
 
11
- ## 2. Start with Static Rendering
19
+ ## Configure JSX
12
20
 
13
- First validate layout and content without streams or the interactive runtime.
21
+ Add this JSX config to your TypeScript configuration so TSX files can use Valyrian's automatic JSX runtime:
14
22
 
15
- ```tsx
16
- /** @jsx v */
17
- /** @jsxFrag v.fragment */
23
+ ```json
24
+ {
25
+ "compilerOptions": {
26
+ "jsx": "react-jsx",
27
+ "jsxImportSource": "valyrian.js"
28
+ }
29
+ }
30
+ ```
31
+
32
+ Manual `v(...)` nodes still work, but the examples below use TSX.
18
33
 
19
- import { v } from "valyrian.js";
20
- import { Screen, Text, renderTerminal } from "valyrianjs-terminal";
34
+ ## Create `main.tsx`
35
+
36
+ Create `main.tsx` with a render-only first pass:
37
+
38
+ ```tsx
39
+ import { Screen, Text, renderTerminal } from "@valyrianjs/terminal";
21
40
 
22
41
  const output = renderTerminal(
23
- <Screen title="Status">
42
+ <Screen title="First terminal UI">
24
43
  <Text>Hello terminal</Text>
25
- <Text>Status: ok</Text>
44
+ <Text>Status: ready</Text>
26
45
  </Screen>
27
46
  );
28
47
 
29
48
  console.log(output);
30
49
  ```
31
50
 
32
- Use this path when you want to:
51
+ ## Run it
33
52
 
34
- - generate snapshots
35
- - validate layout
36
- - test plain output without connecting `stdin` or `stdout`
53
+ Use your TypeScript runner of choice for the run command. With Bun:
37
54
 
38
- ## 3. Move to a Minimal Interactive Session
55
+ ```bash
56
+ bun run main.tsx
57
+ ```
39
58
 
40
- Once you need text editing, focus management, or keyboard handling, switch to `mountTerminal()`.
59
+ Expected plain output:
41
60
 
42
- ```tsx
43
- /** @jsx v */
44
- /** @jsxFrag v.fragment */
61
+ ```text
62
+ First terminal UI
63
+ Hello terminal
64
+ Status: ready
65
+ ```
66
+
67
+ ## Static rendering with `renderTerminal`
68
+
69
+ Use `renderTerminal()` for deterministic plain-text output:
70
+
71
+ - snapshots
72
+ - examples in docs
73
+ - layout checks
74
+ - non-interactive command output
75
+
76
+ This path fits snapshots, documentation examples, layout checks, and non-interactive command output. Move to `mountTerminal()` when the experience needs focus, keyboard input, clipboard behavior, mouse behavior, or repeated renders.
77
+
78
+ ## Interactive sessions with `mountTerminal`
45
79
 
46
- import { v } from "valyrian.js";
47
- import { Button, Input, Screen, Text, mountTerminal } from "valyrianjs-terminal";
80
+ Switch to `mountTerminal()` when the UI needs focus, keyboard input, clipboard behavior, mouse behavior, or repeated renders.
81
+
82
+ Replace `main.tsx` with this interactive version:
83
+
84
+ Full example: [`examples/docs/interactive-note.tsx`](../examples/docs/interactive-note.tsx). Run it with `bun examples/docs/interactive-note.tsx`, type a note, press `Enter` to save, and quit with `Ctrl+C`.
85
+
86
+ ```tsx
87
+ import { Button, Input, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
48
88
 
49
89
  const state = { value: "", saved: "" };
50
90
 
51
- const session = mountTerminal(() => (
52
- <Screen title="Quick Note">
53
- <Input
54
- id="note"
55
- value={state.value}
56
- placeholder="Write a quick note"
57
- onchange={(event) => {
58
- state.value = event.value;
59
- }}
60
- onsubmit={(event) => {
61
- state.saved = event.value;
62
- }}
63
- />
64
- <Button
65
- id="save"
66
- onpress={() => {
67
- state.saved = state.value;
68
- }}
69
- >
70
- Save
71
- </Button>
72
- <Text>{state.saved || "Nothing saved yet"}</Text>
73
- </Screen>
74
- ));
91
+ function App() {
92
+ return (
93
+ <Screen title="Quick note">
94
+ <Input
95
+ id="note"
96
+ value={state.value}
97
+ placeholder="Write a quick note"
98
+ onchange={(event) => {
99
+ state.value = event.value;
100
+ }}
101
+ onsubmit={(event) => {
102
+ state.saved = event.value;
103
+ }}
104
+ />
105
+ <Button
106
+ id="save"
107
+ onpress={() => {
108
+ state.saved = state.value;
109
+ }}
110
+ >
111
+ Save
112
+ </Button>
113
+ <Text>{state.saved || "Nothing saved yet"}</Text>
114
+ </Screen>
115
+ );
116
+ }
117
+
118
+ const session = mountTerminal(<App />);
75
119
 
76
120
  session.focus("note");
77
121
  session.dispatchKey("H");
78
122
  session.dispatchKey("i");
123
+ session.dispatchKey("ENTER");
79
124
 
80
125
  console.log(session.output());
126
+ session.destroy();
81
127
  ```
82
128
 
83
- Practical rule: give stable `id` values to `Input`, `Button`, `List`, and `ScrollView` if you want to use focus, coordinates, or programmatic interaction.
129
+ This example uses programmatic key dispatch for a complete first project. In a real terminal session, pass compatible `stdin` and `stdout` options and call `destroy()` when the session is done.
84
130
 
85
- ## 4. Understand the Minimum Lifecycle
131
+ For styled surfaces and visual states, run `bun examples/docs/style-system.tsx`, try `Tab`, `Enter`, and `W`, and quit with `Ctrl+C`; or open [`examples/docs/style-system.tsx`](../examples/docs/style-system.tsx). For responsive columns and rows, run `bun examples/docs/responsive-split.tsx`, resize the terminal to change the layout, and quit with `Ctrl+C`; or open [`examples/docs/responsive-split.tsx`](../examples/docs/responsive-split.tsx).
86
132
 
87
- The session does not store your application state; it re-evaluates the render function with the current external state.
133
+ ## Add state when the UI grows
88
134
 
89
- ```tsx
90
- /** @jsx v */
91
- /** @jsxFrag v.fragment */
135
+ Small examples can use local state. As the UI grows, keep application state in your app layer and use Valyrian state modules such as `createPulseStore` when named transitions fit your app.
136
+
137
+ Related example: [`examples/docs/component-composition.tsx`](../examples/docs/component-composition.tsx). Run it with `bun examples/docs/component-composition.tsx`, switch cards with `N/P`, and quit with `Ctrl+C`.
92
138
 
93
- import { v } from "valyrian.js";
94
- import { Screen, Text, mountTerminal } from "valyrianjs-terminal";
139
+ ```tsx
140
+ import { createPulseStore } from "valyrian.js/pulses";
141
+ import { Button, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
142
+
143
+ const counter = createPulseStore(
144
+ { count: 0 },
145
+ {
146
+ increment(state) {
147
+ state.count += 1;
148
+ }
149
+ }
150
+ );
95
151
 
96
- const state = { count: 0 };
152
+ function App() {
153
+ return (
154
+ <Screen title="Counter">
155
+ <Text>Count: {counter.state.count}</Text>
156
+ <Button id="increment" onpress={() => counter.increment()}>
157
+ Increment
158
+ </Button>
159
+ </Screen>
160
+ );
161
+ }
97
162
 
98
- const session = mountTerminal(() => (
99
- <Screen>
100
- <Text>Count: {state.count}</Text>
101
- </Screen>
102
- ));
163
+ const session = mountTerminal(<App />);
103
164
 
104
- state.count += 1;
105
- session.update();
165
+ session.focus("increment");
166
+ session.dispatchKey("ENTER");
106
167
 
107
168
  console.log(session.output());
108
169
  session.destroy();
109
170
  ```
110
171
 
111
- Quick summary:
172
+ ValyrianJS Terminal renders the current state and reports events while your app layer owns product state, routing, and workflow choices.
112
173
 
113
- - `output()` returns the current frame as plain text
114
- - `ansiOutput()` returns the current frame as full ANSI output
115
- - `update()` re-evaluates the render function
116
- - `destroy()` removes listeners and cleans up `stdin` when applicable
174
+ ## Compose reusable components
117
175
 
118
- ## 5. When to Connect Streams
176
+ Terminal UIs are regular Valyrian component trees. Extract small components when a screen repeats a pattern, pass scalar props for simple labels, pass object props for domain rows, and render arrays as lists of components.
119
177
 
120
- Connect `stdin`, `stdout`, and `ansi: true` only when you are ready to run a real terminal UI.
178
+ Full example: [`examples/docs/component-composition.tsx`](../examples/docs/component-composition.tsx). Run it with `bun examples/docs/component-composition.tsx`, switch cards with `N/P`, and quit with `Ctrl+C`.
121
179
 
122
180
  ```tsx
123
- /** @jsx v */
124
- /** @jsxFrag v.fragment */
181
+ import { Screen, Text, mountTerminal } from "@valyrianjs/terminal";
125
182
 
126
- import { v } from "valyrian.js";
127
- import { Screen, Text, mountTerminal } from "valyrianjs-terminal";
183
+ function StatusBadge() {
184
+ return <Text>Status: Ready</Text>;
185
+ }
128
186
 
129
- const session = mountTerminal(() => (
130
- <Screen title="Live App">
131
- <Text>Streaming to the terminal</Text>
132
- </Screen>
133
- ), {
134
- stdin: process.stdin,
135
- stdout: process.stdout,
136
- ansi: true
137
- });
187
+ function Greeting({ name }: { name: string }) {
188
+ return <Text>Hello, {name}</Text>;
189
+ }
190
+
191
+ function App() {
192
+ return (
193
+ <Screen title="Components">
194
+ <StatusBadge />
195
+ <Greeting name="Maya" />
196
+ </Screen>
197
+ );
198
+ }
199
+
200
+ const session = mountTerminal(<App />);
201
+
202
+ console.log(session.output());
203
+ session.destroy();
138
204
  ```
139
205
 
140
- For tests or snapshots, `renderTerminal()` or `mountTerminal()` without streams is usually enough.
206
+ `StatusBadge` demonstrates a component with no props: it always renders the same terminal content. `Greeting` demonstrates a scalar prop: the parent keeps the value and passes a plain string into the reusable component.
207
+
208
+ Full example: [`examples/docs/employees-list.tsx`](../examples/docs/employees-list.tsx). Run it with `bun examples/docs/employees-list.tsx`, choose a person with `J/K` or arrows, and quit with `Ctrl+C`.
209
+
210
+ ```tsx
211
+ import { Screen, Text, mountTerminal } from "@valyrianjs/terminal";
212
+
213
+ type EmployeeData = {
214
+ name: string;
215
+ role: string;
216
+ };
217
+
218
+ const employees: EmployeeData[] = [
219
+ { name: "Ana", role: "Design" },
220
+ { name: "Luis", role: "Support" },
221
+ { name: "Maya", role: "Engineering" }
222
+ ];
223
+
224
+ function Employee({ data }: { data: EmployeeData }) {
225
+ return <Text>{`${data.name} — ${data.role}`}</Text>;
226
+ }
227
+
228
+ function Employees({ items }: { items: EmployeeData[] }) {
229
+ return (
230
+ <>
231
+ {items.map((employee) => (
232
+ <Employee data={employee} />
233
+ ))}
234
+ </>
235
+ );
236
+ }
237
+
238
+ function App() {
239
+ return (
240
+ <Screen title="Team">
241
+ <Employees items={employees} />
242
+ </Screen>
243
+ );
244
+ }
245
+
246
+ const session = mountTerminal(<App />);
247
+
248
+ console.log(session.output());
249
+ session.destroy();
250
+ ```
251
+
252
+ `Employee data={...}` demonstrates an object prop for a domain row. `Employees items={...}` demonstrates list composition: the parent owns the array and maps each item into a reusable child component.
253
+
254
+ Use the same composition rules as any declarative terminal tree:
255
+
256
+ - Treat components as small JSX functions that receive props and return terminal elements.
257
+ - The app owns state, data loading, routing decisions, and effects. When components read Valyrian reactive state, external async changes refresh the terminal automatically. Use `session.update()` for non-reactive state or an explicit manual refresh.
258
+ - Use stable `id` values when focus or programmatic dispatch targets a specific interactive node.
259
+ - Keep terminal-specific behavior in public primitives, props, and events instead of hidden component side effects.
141
260
 
142
- ## Next Reading
261
+ ## Next steps
143
262
 
144
- - `docs/core-concepts.md`
145
- - `docs/interaction-model.md`
146
- - `docs/session-runtime.md`
147
- - `docs/cookbook.md`
148
- - `docs/api-reference.md`
263
+ - [Core Concepts](./core-concepts.md) for the package mental model.
264
+ - [Cookbook](./cookbook.md) for practical recipes after the first project.
265
+ - [Interaction Model](./interaction-model.md) for focus, keyboard, mouse, and events.
266
+ - [Session Runtime](./session-runtime.md) for stream and host integration.
267
+ - [API Reference](./api-reference.md) for props, payloads, and session methods.