@flyingrobots/bijou-tui 4.4.0 → 4.4.1
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.
- package/README.md +52 -622
- package/dist/app-frame-actions.d.ts.map +1 -1
- package/dist/app-frame-actions.js +4 -1
- package/dist/app-frame-actions.js.map +1 -1
- package/dist/app-frame-i18n.d.ts.map +1 -1
- package/dist/app-frame-i18n.js +4 -0
- package/dist/app-frame-i18n.js.map +1 -1
- package/dist/app-frame-render.d.ts +8 -8
- package/dist/app-frame-render.d.ts.map +1 -1
- package/dist/app-frame-render.js +82 -23
- package/dist/app-frame-render.js.map +1 -1
- package/dist/app-frame-types.d.ts +3 -0
- package/dist/app-frame-types.d.ts.map +1 -1
- package/dist/app-frame-types.js.map +1 -1
- package/dist/app-frame.d.ts +27 -1
- package/dist/app-frame.d.ts.map +1 -1
- package/dist/app-frame.js +234 -62
- package/dist/app-frame.js.map +1 -1
- package/dist/command-palette.d.ts +5 -3
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +5 -3
- package/dist/command-palette.js.map +1 -1
- package/dist/focus-area.d.ts.map +1 -1
- package/dist/focus-area.js +18 -30
- package/dist/focus-area.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +13 -8
- package/dist/overlay.js.map +1 -1
- package/dist/shell-quit.d.ts +1 -1
- package/dist/shell-quit.d.ts.map +1 -1
- package/dist/shell-quit.js +14 -3
- package/dist/shell-quit.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,45 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flyingrobots/bijou-tui
|
|
2
2
|
|
|
3
3
|
The high-fidelity TEA runtime for Bijou.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@flyingrobots/bijou-tui` provides the application loop, layout primitives, and physics-powered orchestration needed to build complex interactive terminal apps.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Role
|
|
8
8
|
|
|
9
|
-
The
|
|
9
|
+
- **The Elm Architecture (TEA)**: A deterministic state-update-view loop for industrial-strength terminal software.
|
|
10
|
+
- **Fractal TEA**: Compose nested sub-apps with `initSubApp()`, `updateSubApp()`, and `mount()`.
|
|
11
|
+
- **Declarative Motion**: Interpolate layout changes smoothly with physics-based springs and tween animations.
|
|
12
|
+
- **Surface-First Pipeline**: Programmable rendering middleware for fragments, diffing, and shader-based transitions.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- **Pure view contract:** `App.view` and framed pane renderers now speak `ViewOutput` (`Surface | LayoutNode`).
|
|
13
|
-
- **Programmable Rendering Pipeline:** The TEA `view` output is now processed through a 5-stage middleware pipeline (`Layout -> Paint -> PostProcess -> Diff -> Output`). Add custom fragment shaders or logging middleware effortlessly.
|
|
14
|
-
- **Fractal TEA (Sub-Apps):** Compose nested apps with `initSubApp()`, `updateSubApp()`, `mount()`, and `mapCmds()` instead of flattening everything into one update loop.
|
|
15
|
-
- **Bijou CSS (BCSS):** Style supported runtime surface components and frame shell regions with type/class/id selectors, `var()` token lookups, and terminal-aware media queries (`@media (width < 80)`). This is not yet a global cascade across arbitrary layout nodes.
|
|
16
|
-
- **Declarative Motion:** Wrap any component in `motion({ key: 'id' }, ...)` and watch it smoothly interpolate layout changes (move, resize) using physics-based springs.
|
|
17
|
-
- **Unified Heartbeat:** All animations and physics calculations are now synchronized to a single `PulseMsg`, eliminating timer jitter and saving CPU.
|
|
18
|
-
- **Byte-Packed Rendering:** The surface layer is backed by a `Uint8Array` with a zero-alloc `setRGB()` path. The differ compares cells as 10-byte sequences and emits ANSI directly from buffer bytes. All built-in components use the fast path automatically.
|
|
19
|
-
|
|
20
|
-
## Documentation
|
|
21
|
-
|
|
22
|
-
See the [Bijou repo](https://github.com/flyingrobots/bijou) for the full documentation map, architecture guide, design system, and DOGFOOD proving surface.
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
14
|
+
## Install
|
|
25
15
|
|
|
26
16
|
```bash
|
|
27
17
|
npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui
|
|
28
18
|
```
|
|
29
19
|
|
|
30
|
-
If you are upgrading an existing app, see [`../../docs/MIGRATING_TO_V4.md`](../../docs/MIGRATING_TO_V4.md).
|
|
31
|
-
|
|
32
20
|
## Quick Start (Sub-App Composition)
|
|
33
21
|
|
|
34
22
|
```typescript
|
|
35
23
|
import { initDefaultContext } from '@flyingrobots/bijou-node';
|
|
36
|
-
import { run, mount,
|
|
37
|
-
import { createSurface
|
|
24
|
+
import { run, mount, type App } from '@flyingrobots/bijou-tui';
|
|
25
|
+
import { createSurface } from '@flyingrobots/bijou';
|
|
38
26
|
|
|
39
27
|
initDefaultContext();
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
type ChildModel = { count: number };
|
|
30
|
+
type ChildMsg = { type: 'noop' };
|
|
31
|
+
type Model = { left: ChildModel; right: ChildModel };
|
|
32
|
+
type Msg = { pane: 'left' | 'right'; msg: ChildMsg };
|
|
33
|
+
|
|
34
|
+
const childApp: App<ChildModel, ChildMsg> = {
|
|
43
35
|
init: () => [{ count: 0 }, []],
|
|
44
36
|
update: (msg, model) => [model, []],
|
|
45
37
|
view: (model) => {
|
|
@@ -49,24 +41,22 @@ const childApp: App<{ count: number }, any> = {
|
|
|
49
41
|
}
|
|
50
42
|
};
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
left: { count: number };
|
|
54
|
-
right: { count: number };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Parent App mounting two independent Sub-Apps
|
|
58
|
-
const app: App<Model, any> = {
|
|
44
|
+
const app: App<Model, Msg> = {
|
|
59
45
|
init: () => [{ left: { count: 0 }, right: { count: 0 } }, []],
|
|
60
46
|
update: (msg, model) => [model, []],
|
|
61
47
|
view: (model) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
const [left] = mount(childApp, {
|
|
49
|
+
model: model.left,
|
|
50
|
+
onMsg: (msg) => ({ pane: 'left', msg }),
|
|
51
|
+
});
|
|
52
|
+
const [right] = mount(childApp, {
|
|
53
|
+
model: model.right,
|
|
54
|
+
onMsg: (msg) => ({ pane: 'right', msg }),
|
|
55
|
+
});
|
|
65
56
|
|
|
66
|
-
// Composite them onto the main screen
|
|
67
57
|
const screen = createSurface(80, 24);
|
|
68
|
-
screen.blit(
|
|
69
|
-
screen.blit(
|
|
58
|
+
screen.blit(left, 0, 0);
|
|
59
|
+
screen.blit(right, 40, 0);
|
|
70
60
|
return screen;
|
|
71
61
|
}
|
|
72
62
|
};
|
|
@@ -74,617 +64,57 @@ const app: App<Model, any> = {
|
|
|
74
64
|
run(app);
|
|
75
65
|
```
|
|
76
66
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
import { initDefaultContext } from '@flyingrobots/bijou-node';
|
|
81
|
-
import { stringToSurface } from '@flyingrobots/bijou';
|
|
82
|
-
import { run, quit, type App, isKeyMsg } from '@flyingrobots/bijou-tui';
|
|
83
|
-
|
|
84
|
-
initDefaultContext();
|
|
85
|
-
|
|
86
|
-
type Model = { count: number };
|
|
87
|
-
|
|
88
|
-
const app: App<Model> = {
|
|
89
|
-
init: () => [{ count: 0 }, []],
|
|
90
|
-
|
|
91
|
-
update: (msg, model) => {
|
|
92
|
-
if (isKeyMsg(msg)) {
|
|
93
|
-
if (msg.key === 'q') return [model, [quit()]];
|
|
94
|
-
if (msg.key === '+') return [{ count: model.count + 1 }, []];
|
|
95
|
-
if (msg.key === '-') return [{ count: model.count - 1 }, []];
|
|
96
|
-
}
|
|
97
|
-
return [model, []];
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
view: (model) => {
|
|
101
|
-
const text = `Count: ${model.count}\n\nPress +/- to change, q to quit`;
|
|
102
|
-
const lines = text.split('\n');
|
|
103
|
-
return stringToSurface(text, Math.max(1, ...lines.map((line) => line.length)), lines.length);
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
run(app);
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Runtime Behavior Note
|
|
111
|
-
|
|
112
|
-
`run()` behaves differently by output mode:
|
|
113
|
-
|
|
114
|
-
- `interactive`: full TEA loop (event bus, key/resize/mouse handling, command-driven updates).
|
|
115
|
-
- `pipe` / `static` / `accessible`: render `view(initModel)` once and return immediately.
|
|
116
|
-
|
|
117
|
-
In non-interactive modes, there is no normal interactive event loop.
|
|
118
|
-
|
|
119
|
-
## Command Model
|
|
67
|
+
## Strategy: Choosing Component Families
|
|
120
68
|
|
|
121
|
-
|
|
69
|
+
Select the family based on the interaction semantic.
|
|
122
70
|
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
71
|
+
### Overlays and Interruption
|
|
72
|
+
- **`drawer()`**: Supplemental detail while maintaining main context.
|
|
73
|
+
- **`modal()`**: Required decision that blocks background activity.
|
|
74
|
+
- **`toast()`**: Transient notification for a single event.
|
|
75
|
+
- **`tooltip()`**: Micro-explanation for a local target.
|
|
127
76
|
|
|
128
|
-
|
|
77
|
+
### Collection Interaction
|
|
78
|
+
- **`navigableTable()`**: Keyboard-driven traversal and cell inspection.
|
|
79
|
+
- **`browsableList()`**: Description-led traversal in one dimension.
|
|
80
|
+
- **`commandPalette()`**: Action discovery and navigation.
|
|
129
81
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
- **Input architecture**: keymaps, grouped bindings, generated help views, and layered input stack for modal flows.
|
|
136
|
-
- **Overlay composition**: modal, toast, drawer, tooltip, and painter-style compositing primitives (including panel-scoped drawers).
|
|
137
|
-
- **App shell**: `createFramedApp()` for tabs/help/chrome/pane-focus boilerplate with optional command palette.
|
|
138
|
-
- **Stateful building blocks**: navigable table, browsable list, file picker, focus area, and DAG pane with vim-friendly keymaps.
|
|
139
|
-
|
|
140
|
-
## Choosing Component Families
|
|
141
|
-
|
|
142
|
-
### Overlays and interruption
|
|
143
|
-
|
|
144
|
-
- Use `toast()` when you are composing a single transient overlay directly.
|
|
145
|
-
- Use the notification system when the app needs stacking, placement, actions, routing, or history.
|
|
146
|
-
- Use `drawer()` when the user should keep the main surface visible while working in supplemental detail.
|
|
147
|
-
- Use `modal()` when background shortcuts and pointer actions should be blocked.
|
|
148
|
-
- Use `tooltip()` only for tiny local explanation, not for decisions or scrollable content.
|
|
149
|
-
- If the overlay needs embedded component surfaces or multiple real rows, keep it on the structured `Surface` path with `compositeSurface()`.
|
|
150
|
-
|
|
151
|
-
### Collection interaction
|
|
152
|
-
|
|
153
|
-
- Use core `table()` or `tableSurface()` for passive comparison.
|
|
154
|
-
- Use `navigableTable()` when row/cell focus and keyboard traversal are the real job.
|
|
155
|
-
- Use `browsableList()` when the content is one-dimensional and description-led rather than grid-oriented.
|
|
156
|
-
- Use `commandPaletteSurface()` when the outcome is an action or navigation target, not a stored form value.
|
|
157
|
-
- If users are really choosing persisted values, keep that work in core `select()` / `filter()` / `multiselect()` instead of turning the palette into a value picker.
|
|
158
|
-
|
|
159
|
-
### Shell and workspace layout
|
|
160
|
-
|
|
161
|
-
- Use `createFramedApp()` when the app has multiple destinations, overlays, and workspace state that should be standardized.
|
|
162
|
-
- Use `splitPane()` when the user benefits from primary-versus-secondary context or side-by-side comparison.
|
|
163
|
-
- Use `grid()` when multiple stable regions deserve simultaneous visibility.
|
|
164
|
-
- Use `statusBarSurface()` when shell chrome already lives on the structured `Surface` path; keep `statusBar()` for explicit text output.
|
|
165
|
-
- Use `helpShortSurface()` or `helpViewSurface()` when shortcut guidance stays inside the rich shell; keep `helpShort()` / `helpView()` for explicit text output.
|
|
166
|
-
- Use `commandPaletteSurface()` for action discovery and navigation inside the shell, not as a substitute value picker.
|
|
167
|
-
- Use notifications for events and follow-up, not as a replacement for the status rail.
|
|
168
|
-
- Keep status rails concise and global; explanatory text belongs in the page, not in shell chrome.
|
|
169
|
-
- Mouse is enhancement, not baseline. Overlay layers should consume pointer input before shell chrome or page content, and every click target should mirror an existing keyboard path.
|
|
82
|
+
### Shell and Workspace Layout
|
|
83
|
+
- **`createFramedApp()`**: Batteries-included workspace with tabs, panes, and help.
|
|
84
|
+
- **`splitPane()`**: Dynamic primary/secondary context comparison.
|
|
85
|
+
- **`grid()`**: Stable regions with simultaneous visibility.
|
|
86
|
+
- **`viewport()`**: The canonical scroll mask for rich composition.
|
|
170
87
|
|
|
171
88
|
## Animation
|
|
172
89
|
|
|
173
90
|
### Spring Physics
|
|
174
|
-
|
|
175
91
|
```typescript
|
|
176
92
|
import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
|
|
177
93
|
|
|
178
|
-
// Physics-based (default) — runs until the spring settles
|
|
179
94
|
const cmd = animate({
|
|
180
95
|
from: 0,
|
|
181
96
|
to: 100,
|
|
182
|
-
spring: 'wobbly',
|
|
183
|
-
onFrame: (v) => ({ type: 'scroll', y: v }),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Duration-based with easing
|
|
187
|
-
const fade = animate({
|
|
188
|
-
type: 'tween',
|
|
189
|
-
from: 0,
|
|
190
|
-
to: 1,
|
|
191
|
-
duration: 300,
|
|
192
|
-
ease: EASINGS.easeOutCubic,
|
|
193
|
-
onFrame: (v) => ({ type: 'fade', opacity: v }),
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Skip animation (reduced motion)
|
|
197
|
-
const jump = animate({
|
|
198
|
-
from: 0, to: 100,
|
|
199
|
-
immediate: true,
|
|
97
|
+
spring: 'wobbly',
|
|
200
98
|
onFrame: (v) => ({ type: 'scroll', y: v }),
|
|
201
99
|
});
|
|
202
100
|
```
|
|
203
101
|
|
|
204
|
-
### Timeline
|
|
205
|
-
|
|
206
|
-
GSAP-style orchestration — pure state machine, no timers:
|
|
207
|
-
|
|
102
|
+
### Timeline Orchestration
|
|
208
103
|
```typescript
|
|
209
104
|
import { timeline } from '@flyingrobots/bijou-tui';
|
|
210
105
|
|
|
211
106
|
const tl = timeline()
|
|
212
|
-
.add('slideIn',
|
|
213
|
-
.add('fadeIn', { type: 'tween', from: 0, to: 1, duration: 200 }, '-=100')
|
|
107
|
+
.add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
|
|
214
108
|
.label('settled')
|
|
215
|
-
.add('bounce',
|
|
216
|
-
.call('onReady', 'settled+=50')
|
|
109
|
+
.add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
|
|
217
110
|
.build();
|
|
218
|
-
|
|
219
|
-
// Drive from TEA update:
|
|
220
|
-
let tlState = tl.init();
|
|
221
|
-
// on each frame:
|
|
222
|
-
tlState = tl.step(tlState, 1/60);
|
|
223
|
-
const { slideIn, fadeIn, bounce } = tl.values(tlState);
|
|
224
|
-
const fired = tl.firedCallbacks(prev, tlState); // ['onReady']
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
Position syntax: `'<'` (parallel), `'+=N'` (gap), `'-=N'` (overlap), `'<+=N'` (offset from previous start), absolute ms, `'label'`, `'label+=N'`.
|
|
228
|
-
|
|
229
|
-
## Transition Shaders
|
|
230
|
-
|
|
231
|
-
Custom page transitions are surface-native in v4. Shader functions decide whether each cell shows the previous page or next page, and may optionally provide override data for that cell.
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
import { type TransitionShaderFn } from '@flyingrobots/bijou-tui';
|
|
235
|
-
|
|
236
|
-
const shimmer: TransitionShaderFn = ({ progress, x, width }) => {
|
|
237
|
-
const edge = Math.floor(progress * width);
|
|
238
|
-
if (x < edge) return { showNext: true };
|
|
239
|
-
if (x === edge) return { showNext: false, overrideChar: '░', overrideRole: 'marker' };
|
|
240
|
-
return { showNext: false };
|
|
241
|
-
};
|
|
242
111
|
```
|
|
243
112
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Use transition shaders to reinforce workspace change, not as default spectacle. If the effect makes the new page harder to read or hides state meaning that should remain explicit in static or accessible modes, it is the wrong transition.
|
|
247
|
-
|
|
248
|
-
## Layout
|
|
249
|
-
|
|
250
|
-
### Flexbox
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
import { flex } from '@flyingrobots/bijou-tui';
|
|
254
|
-
|
|
255
|
-
// Sidebar + main content, responsive to terminal width
|
|
256
|
-
flex({ direction: 'row', width: cols, height: rows, gap: 1 },
|
|
257
|
-
{ basis: 20, content: sidebarText },
|
|
258
|
-
{ flex: 1, content: (w, h) => renderMain(w, h) },
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
// Header + body + footer
|
|
262
|
-
flex({ direction: 'column', width: cols, height: rows },
|
|
263
|
-
{ basis: 1, content: headerLine },
|
|
264
|
-
{ flex: 1, content: (w, h) => renderBody(w, h) },
|
|
265
|
-
{ basis: 1, content: statusLine },
|
|
266
|
-
);
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Children can be **render functions** `(width, height) => string` — they receive their allocated space and reflow automatically when the terminal resizes.
|
|
270
|
-
|
|
271
|
-
### Viewport
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import { viewportSurface, createScrollStateForContent, scrollBy, pageDown } from '@flyingrobots/bijou-tui';
|
|
275
|
-
import { boxSurface } from '@flyingrobots/bijou';
|
|
276
|
-
|
|
277
|
-
const content = boxSurface(longText, { width: 72 });
|
|
278
|
-
let scroll = createScrollStateForContent(content, viewportHeight);
|
|
279
|
-
|
|
280
|
-
// Mask the content to a visible window with scrollbar
|
|
281
|
-
const view = viewportSurface({ width: 60, height: 20, content, scrollY: scroll.y });
|
|
282
|
-
|
|
283
|
-
// Handle scroll keys
|
|
284
|
-
scroll = scrollBy(scroll, 1); // down one line
|
|
285
|
-
scroll = pageDown(scroll); // down one page
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Treat `viewportSurface()` as the canonical scroll mask for rich TUI composition. Keep `viewport()` for explicit text-lowering paths.
|
|
289
|
-
|
|
290
|
-
### Basic Layout
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
import {
|
|
294
|
-
hstack,
|
|
295
|
-
hstackSurface,
|
|
296
|
-
place,
|
|
297
|
-
placeSurface,
|
|
298
|
-
vstack,
|
|
299
|
-
vstackSurface,
|
|
300
|
-
} from '@flyingrobots/bijou-tui';
|
|
301
|
-
|
|
302
|
-
vstack(header, content, footer); // explicit text-lowering path
|
|
303
|
-
hstack(2, leftPanel, rightPanel); // explicit text-lowering path
|
|
304
|
-
place('Title', { width: 20, height: 3 }); // text placement
|
|
305
|
-
|
|
306
|
-
vstackSurface(headerSurface, bodySurface); // structured surface stack
|
|
307
|
-
hstackSurface(2, navSurface, mainSurface); // structured horizontal stack
|
|
308
|
-
placeSurface(dialogSurface, { // structured placement/alignment
|
|
309
|
-
width: cols,
|
|
310
|
-
height: rows,
|
|
311
|
-
hAlign: 'center',
|
|
312
|
-
vAlign: 'middle',
|
|
313
|
-
});
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Prefer `vstackSurface()` / `hstackSurface()` / `placeSurface()` when the view is already composed from `Surface` values. Keep `vstack()` / `hstack()` / `place()` for explicit text composition or lowering paths.
|
|
317
|
-
|
|
318
|
-
### Split Pane
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
import {
|
|
322
|
-
createSplitPaneState, splitPaneSurface, splitPaneResizeBy, splitPaneFocusNext,
|
|
323
|
-
} from '@flyingrobots/bijou-tui';
|
|
324
|
-
|
|
325
|
-
let state = createSplitPaneState({ ratio: 0.35 });
|
|
326
|
-
|
|
327
|
-
// in update:
|
|
328
|
-
state = splitPaneResizeBy(state, 2, { total: cols, minA: 16, minB: 16 });
|
|
329
|
-
state = splitPaneFocusNext(state);
|
|
330
|
-
|
|
331
|
-
// in view:
|
|
332
|
-
const output = splitPaneSurface(state, {
|
|
333
|
-
direction: 'row',
|
|
334
|
-
width: cols,
|
|
335
|
-
height: rows,
|
|
336
|
-
minA: 16,
|
|
337
|
-
minB: 16,
|
|
338
|
-
paneA: (w, h) => renderSidebar(w, h),
|
|
339
|
-
paneB: (w, h) => renderMain(w, h),
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
Prefer `splitPaneSurface()` when the panes are already structured `Surface` views. Keep `splitPane()` for explicit text composition or lowering paths.
|
|
344
|
-
|
|
345
|
-
### Grid
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import { gridSurface } from '@flyingrobots/bijou-tui';
|
|
349
|
-
|
|
350
|
-
const output = gridSurface({
|
|
351
|
-
width: cols,
|
|
352
|
-
height: rows,
|
|
353
|
-
columns: [24, '1fr'],
|
|
354
|
-
rows: [3, '1fr', 8],
|
|
355
|
-
areas: [
|
|
356
|
-
'header header',
|
|
357
|
-
'nav main',
|
|
358
|
-
'logs main',
|
|
359
|
-
],
|
|
360
|
-
gap: 1,
|
|
361
|
-
cells: {
|
|
362
|
-
header: (w, h) => renderHeader(w, h),
|
|
363
|
-
nav: (w, h) => renderNav(w, h),
|
|
364
|
-
logs: (w, h) => renderLogs(w, h),
|
|
365
|
-
main: (w, h) => renderMain(w, h),
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
Prefer `gridSurface()` when the regions are already structured `Surface` views. Keep `grid()` for explicit text composition or lowering paths.
|
|
371
|
-
|
|
372
|
-
## Resize Handling
|
|
373
|
-
|
|
374
|
-
Terminal resize events are dispatched automatically as `ResizeMsg`:
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
update(msg, model) {
|
|
378
|
-
if (msg.type === 'resize') {
|
|
379
|
-
return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
|
|
380
|
-
}
|
|
381
|
-
// ...
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
view(model) {
|
|
385
|
-
return flex(
|
|
386
|
-
{ direction: 'row', width: model.cols, height: model.rows },
|
|
387
|
-
{ basis: 20, content: sidebar },
|
|
388
|
-
{ flex: 1, content: (w, h) => mainContent(w, h) },
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
## Event Bus
|
|
394
|
-
|
|
395
|
-
The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
import { createEventBus } from '@flyingrobots/bijou-tui';
|
|
399
|
-
|
|
400
|
-
const bus = createEventBus<MyMsg>();
|
|
401
|
-
bus.connectIO(ctx.io); // keyboard + resize
|
|
402
|
-
bus.on((msg) => { /* ... */ }); // single subscription
|
|
403
|
-
bus.emit(customMsg); // synthetic events
|
|
404
|
-
bus.runCmd(someCommand); // final messages re-emitted, cleanup retained
|
|
405
|
-
bus.dispose(); // clean shutdown
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full event flow and [GUIDE.md](./GUIDE.md) for detailed usage patterns.
|
|
409
|
-
|
|
410
|
-
## Keybinding Manager
|
|
411
|
-
|
|
412
|
-
Declarative key binding with modifier support, named groups, and runtime enable/disable:
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';
|
|
416
|
-
|
|
417
|
-
type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };
|
|
418
|
-
|
|
419
|
-
const kb = createKeyMap<Msg>()
|
|
420
|
-
.bind('q', 'Quit', { type: 'quit' })
|
|
421
|
-
.bind('?', 'Help', { type: 'help' })
|
|
422
|
-
.bind('ctrl+c', 'Force quit', { type: 'quit' })
|
|
423
|
-
.group('Navigation', (g) => g
|
|
424
|
-
.bind('j', 'Down', { type: 'move', dir: 'down' })
|
|
425
|
-
.bind('k', 'Up', { type: 'move', dir: 'up' })
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
// In TEA update:
|
|
429
|
-
const action = kb.handle(keyMsg);
|
|
430
|
-
if (action !== undefined) return [model, [/* ... */]];
|
|
431
|
-
|
|
432
|
-
// Runtime enable/disable
|
|
433
|
-
kb.disableGroup('Navigation');
|
|
434
|
-
kb.enable('Quit');
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Help Generation
|
|
438
|
-
|
|
439
|
-
Auto-generate help text from registered bindings:
|
|
440
|
-
|
|
441
|
-
```typescript
|
|
442
|
-
import {
|
|
443
|
-
helpView,
|
|
444
|
-
helpViewSurface,
|
|
445
|
-
helpShort,
|
|
446
|
-
helpShortSurface,
|
|
447
|
-
helpFor,
|
|
448
|
-
helpForSurface,
|
|
449
|
-
} from '@flyingrobots/bijou-tui';
|
|
450
|
-
|
|
451
|
-
helpView(kb); // full grouped multi-line help
|
|
452
|
-
helpShort(kb); // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
|
|
453
|
-
helpFor(kb, 'Nav'); // only Navigation group
|
|
454
|
-
helpShortSurface(kb, { width: 48 }); // shell hint that stays on the Surface path
|
|
455
|
-
helpViewSurface(kb, { width: 48 }); // grouped help as a Surface
|
|
456
|
-
helpForSurface(kb, 'Nav', { width: 48 });
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Input Stack
|
|
460
|
-
|
|
461
|
-
Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
464
|
-
import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';
|
|
465
|
-
|
|
466
|
-
const stack = createInputStack<KeyMsg, Msg>();
|
|
467
|
-
|
|
468
|
-
// Base layer — global keys, lets unmatched events fall through
|
|
469
|
-
stack.push(appKeys, { passthrough: true });
|
|
470
|
-
|
|
471
|
-
// Modal opens — captures all input (opaque by default)
|
|
472
|
-
const modalId = stack.push(modalKeys);
|
|
473
|
-
|
|
474
|
-
// Dispatch returns first matched action, top-down
|
|
475
|
-
const action = stack.dispatch(keyMsg);
|
|
476
|
-
|
|
477
|
-
// Modal closes
|
|
478
|
-
stack.remove(modalId);
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
`KeyMap` implements `InputHandler`, so it plugs directly into the input stack.
|
|
482
|
-
|
|
483
|
-
## Overlay Compositing
|
|
484
|
-
|
|
485
|
-
Paint overlays (modals, toasts) on top of existing content:
|
|
486
|
-
|
|
487
|
-
```typescript
|
|
488
|
-
import { compositeSurface, modal, toast } from '@flyingrobots/bijou-tui';
|
|
489
|
-
|
|
490
|
-
// Create a centered dialog
|
|
491
|
-
const dialog = modal({
|
|
492
|
-
title: 'Confirm',
|
|
493
|
-
body: 'Delete this item?',
|
|
494
|
-
hint: 'y/n',
|
|
495
|
-
screenWidth: 80,
|
|
496
|
-
screenHeight: 24,
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Create a toast notification
|
|
500
|
-
const notification = toast({
|
|
501
|
-
message: 'Saved successfully',
|
|
502
|
-
variant: 'success', // 'success' | 'error' | 'info'
|
|
503
|
-
anchor: 'bottom-right', // 'top-right' | 'bottom-right' | 'bottom-left' | 'top-left'
|
|
504
|
-
screenWidth: 80,
|
|
505
|
-
screenHeight: 24,
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// Paint overlays onto background content
|
|
509
|
-
const output = compositeSurface(backgroundSurface, [dialog, notification], { dim: true });
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
Each overlay now exposes both `surface` and `content` forms. Prefer `compositeSurface()` when your app is already on the surface-native path. Keep the string-oriented `composite()` path for explicit lowering boundaries, not as the default mental model. The `dim` option fades the background with ANSI dim.
|
|
513
|
-
|
|
514
|
-
`modal().body`, `modal().hint`, `drawer().content`, and `tooltip().content` accept either plain strings or structured `Surface` content. Use surfaces when the overlay needs real rows, embedded component surfaces, or richer composition inside the interrupting layer.
|
|
515
|
-
|
|
516
|
-
Reach for `toast()` when the app is composing a one-off overlay directly. Reach for the notification system when stacking, actions, routing, or history matter. The notification lab in `examples/notifications` is the canonical higher-level example.
|
|
517
|
-
|
|
518
|
-
`drawer()` now supports `left`/`right`/`top`/`bottom` anchors and optional `region` mounting for panel-scoped overlays.
|
|
519
|
-
Use `drawer()` when the user should keep the main task visible while consulting or editing supplemental context. Use `modal()` when the user must stop and decide. Use `tooltip()` only for tiny local explanation, not for commands or scrollable content.
|
|
520
|
-
|
|
521
|
-
## App Frame
|
|
522
|
-
|
|
523
|
-
`createFramedApp()` wraps page-level TEA logic in a shared shell:
|
|
524
|
-
|
|
525
|
-
- tabs + page switching
|
|
526
|
-
- pane focus and per-pane scroll isolation
|
|
527
|
-
- frame help (`?`) and optional command palette (`ctrl+p` / `:`)
|
|
528
|
-
- overlay factory with pane rects for panel-scoped drawers/modals
|
|
529
|
-
|
|
530
|
-
Pane renderers return a `Surface` or a `LayoutNode`. The shell normalizes those outputs into the framed scroll/focus path for you.
|
|
531
|
-
|
|
532
|
-
Typed shell notes:
|
|
533
|
-
|
|
534
|
-
- `createFramedApp<PageModel, Msg>()` returns `FramedApp<PageModel, Msg>`
|
|
535
|
-
- page `update()` receives `FramePageMsg<Msg>` so raw `mouse` / `pulse` delivery is explicit
|
|
536
|
-
- wrapped shell commands carry `FramedAppMsg<Msg>` instead of collapsing frame/page wrappers into plain `Msg`
|
|
537
|
-
|
|
538
|
-
See `examples/release-workbench/main.ts` for the canonical shell demo and `examples/app-frame/main.ts` for a compact focused example.
|
|
539
|
-
|
|
540
|
-
Shell role split matters:
|
|
541
|
-
|
|
542
|
-
- `statusBarSurface()` communicates concise global state
|
|
543
|
-
- `helpShortSurface()` / `helpViewSurface()` teach shortcuts and scope
|
|
544
|
-
- `commandPaletteSurface()` handles action discovery and navigation
|
|
545
|
-
- notifications surface events and follow-up
|
|
546
|
-
|
|
547
|
-
## Building Blocks
|
|
548
|
-
|
|
549
|
-
Reusable stateful components that follow the TEA state + pure transformers + sync render + convenience keymap pattern:
|
|
550
|
-
|
|
551
|
-
### Navigable Table
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
import {
|
|
555
|
-
createNavigableTableState, navigableTable, navigableTableSurface, navTableFocusNext,
|
|
556
|
-
navTableKeyMap, helpShort,
|
|
557
|
-
} from '@flyingrobots/bijou-tui';
|
|
558
|
-
|
|
559
|
-
const state = createNavigableTableState({ columns, rows, height: 10 });
|
|
560
|
-
const textOutput = navigableTable(state, { ctx });
|
|
561
|
-
const surfaceOutput = navigableTableSurface(state, { ctx });
|
|
562
|
-
const next = navTableFocusNext(state);
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
Use `navigableTableSurface()` when the user should actively traverse a table inside a rich TUI surface. Keep `navigableTable()` for explicit text lowering. If the job is still passive comparison, prefer core `table()` or `tableSurface()` and keep the interaction layer simpler.
|
|
566
|
-
|
|
567
|
-
### Browsable List
|
|
568
|
-
|
|
569
|
-
```typescript
|
|
570
|
-
import {
|
|
571
|
-
createBrowsableListState, browsableList, browsableListSurface, listFocusNext,
|
|
572
|
-
browsableListKeyMap,
|
|
573
|
-
} from '@flyingrobots/bijou-tui';
|
|
574
|
-
|
|
575
|
-
const state = createBrowsableListState({ items, height: 10 });
|
|
576
|
-
const textOutput = browsableList(state);
|
|
577
|
-
const surfaceOutput = browsableListSurface(state, { width: 40 });
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
Use `browsableListSurface()` when the list belongs inside a rich TUI region and should share viewport masking semantics with pagers and focus areas. Keep `browsableList()` for explicit text lowering.
|
|
581
|
-
|
|
582
|
-
### File Picker
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
import {
|
|
586
|
-
createFilePickerState, filePicker, filePickerSurface, fpFocusNext, fpEnter, fpBack,
|
|
587
|
-
filePickerKeyMap,
|
|
588
|
-
} from '@flyingrobots/bijou-tui';
|
|
589
|
-
import { nodeIO } from '@flyingrobots/bijou-node';
|
|
590
|
-
|
|
591
|
-
const io = nodeIO();
|
|
592
|
-
const state = createFilePickerState({ cwd: process.cwd(), io, height: 15 });
|
|
593
|
-
const textOutput = filePicker(state);
|
|
594
|
-
const surfaceOutput = filePickerSurface(state, { width: 60 });
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
Use `filePickerSurface()` when the browser lives inside a rich TUI pane and should inherit shared viewport masking semantics. Keep `filePicker()` for explicit text lowering.
|
|
598
|
-
|
|
599
|
-
### Command Palette
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
import {
|
|
603
|
-
createCommandPaletteState, commandPalette, commandPaletteSurface,
|
|
604
|
-
cpFilter, commandPaletteKeyMap,
|
|
605
|
-
} from '@flyingrobots/bijou-tui';
|
|
606
|
-
|
|
607
|
-
const state = createCommandPaletteState(items, 8);
|
|
608
|
-
const textOutput = commandPalette(state, { width: 60 });
|
|
609
|
-
const surfaceOutput = commandPaletteSurface(state, { width: 60, ctx });
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
Use `commandPaletteSurface()` when the palette is part of a structured shell or overlay and should share viewport masking semantics. Keep `commandPalette()` for explicit text lowering.
|
|
613
|
-
|
|
614
|
-
### Pager
|
|
615
|
-
|
|
616
|
-
```typescript
|
|
617
|
-
import {
|
|
618
|
-
createPagerStateForSurface,
|
|
619
|
-
pagerSurface,
|
|
620
|
-
pagerScrollBy,
|
|
621
|
-
} from '@flyingrobots/bijou-tui';
|
|
622
|
-
|
|
623
|
-
const state = createPagerStateForSurface(contentSurface, { width: 60, height: 20 });
|
|
624
|
-
const output = pagerSurface(contentSurface, state);
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### Focus Area
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
import {
|
|
631
|
-
createFocusAreaStateForSurface, focusAreaScrollBy, focusAreaSurface,
|
|
632
|
-
focusAreaKeyMap,
|
|
633
|
-
} from '@flyingrobots/bijou-tui';
|
|
634
|
-
|
|
635
|
-
const state = createFocusAreaStateForSurface(contentSurface, {
|
|
636
|
-
width: 60,
|
|
637
|
-
height: 20,
|
|
638
|
-
overflowX: 'scroll',
|
|
639
|
-
});
|
|
640
|
-
const output = focusAreaSurface(contentSurface, state, { focused: true, ctx });
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
If the pane is still intentionally text-composed, `createFocusAreaState()` + `focusArea()` remain the explicit lowering path.
|
|
644
|
-
|
|
645
|
-
### DAG Pane
|
|
646
|
-
|
|
647
|
-
Use `dagPane()` when graph inspection is an active task and the user needs keyboard-owned selection, path highlighting, and scroll control. Keep plain `dag()` in `@flyingrobots/bijou` for passive graph explanation, and move to `dagSlice()` or `dagStats()` when a focused fragment or structural summary would be more honest than a full interactive graph.
|
|
648
|
-
|
|
649
|
-
```typescript
|
|
650
|
-
import {
|
|
651
|
-
createDagPaneState, dagPane, dagPaneSelectChild,
|
|
652
|
-
dagPaneSelectParent, dagPaneKeyMap,
|
|
653
|
-
} from '@flyingrobots/bijou-tui';
|
|
654
|
-
|
|
655
|
-
const state = createDagPaneState({ source: nodes, width: 80, height: 24, ctx });
|
|
656
|
-
const output = dagPane(state, { focused: true, ctx });
|
|
657
|
-
const next = dagPaneSelectChild(state, ctx); // arrow-key navigation
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
All building blocks include `*KeyMap()` factories for preconfigured vim-style keybindings.
|
|
661
|
-
|
|
662
|
-
## Related Packages
|
|
663
|
-
|
|
664
|
-
- [`@flyingrobots/bijou`](https://www.npmjs.com/package/@flyingrobots/bijou) — Zero-dependency core with all components and theme engine
|
|
665
|
-
- [`@flyingrobots/bijou-node`](https://www.npmjs.com/package/@flyingrobots/bijou-node) — Node.js runtime adapter (chalk, readline, process)
|
|
666
|
-
|
|
667
|
-
## License
|
|
113
|
+
## Documentation
|
|
668
114
|
|
|
669
|
-
|
|
115
|
+
- **[GUIDE.md](./GUIDE.md)**: Productive-fast path for building apps.
|
|
116
|
+
- **[ADVANCED_GUIDE.md](./ADVANCED_GUIDE.md)**: Shell doctrine, shaders, and motion internals.
|
|
117
|
+
- **[Design System](../../docs/design-system/README.md)**: Semantic guidance and patterns.
|
|
670
118
|
|
|
671
119
|
---
|
|
672
|
-
|
|
673
|
-
<p align="center">
|
|
674
|
-
Built with 💎 by <a href="https://github.com/flyingrobots">FLYING ROBOTS</a>
|
|
675
|
-
</p>
|
|
676
|
-
|
|
677
|
-
```rust
|
|
678
|
-
.-:::::'::: .-:. ::-.::::::. :::. .,-:::::/
|
|
679
|
-
;;;'''' ;;; ';;. ;;;;';;;`;;;;, `;;;,;;-'````'
|
|
680
|
-
[[[,,== [[[ '[[,[[[' [[[ [[[[[. '[[[[[ [[[[[[/
|
|
681
|
-
`$$$"`` $$' c$$" $$$ $$$ "Y$c$$"$$c. "$$
|
|
682
|
-
888 o88oo,.__ ,8P"` 888 888 Y88 `Y8bo,,,o88o
|
|
683
|
-
"MM, """"YUMMMmM" MMM MMM YM `'YMUP"YMM
|
|
684
|
-
:::::::.. ... :::::::. ... :::::::::::: .::::::.
|
|
685
|
-
;;;;``;;;; .;;;;;;;. ;;;'';;' .;;;;;;;.;;;;;;;;'''';;;` `
|
|
686
|
-
[[[,/[[[' ,[[ \[[, [[[__[[\.,[[ \[[, [[ '[==/[[[[,
|
|
687
|
-
$$$$$$c $$$, $$$ $$""""Y$$$$$, $$$ $$ ''' $
|
|
688
|
-
888b "88bo,"888,_ _,88P_88o,,od8P"888,_ _,88P 88, 88b dP
|
|
689
|
-
MMMM "W" "YMMMMMP" ""YUMMMP" "YMMMMMP" MMM "YMmMY"
|
|
690
|
-
```
|
|
120
|
+
Built with 💎 by [FLYING ROBOTS](https://github.com/flyingrobots)
|